mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-09-20 08:45:24 +01:00
Merge branch 'develop' into develop
This commit is contained in:
@@ -8,7 +8,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
|
||||
+72
-89
@@ -1,91 +1,74 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"!**/dist/**/*"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 4,
|
||||
"lineWidth": 120,
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
":BUN:",
|
||||
":NODE:",
|
||||
[
|
||||
"npm:*",
|
||||
"npm:*/**"
|
||||
],
|
||||
":PACKAGE_WITH_PROTOCOL:",
|
||||
":URL:",
|
||||
":PACKAGE:",
|
||||
[
|
||||
"/src/*",
|
||||
"/src/**"
|
||||
],
|
||||
[
|
||||
"/**"
|
||||
],
|
||||
[
|
||||
"#*",
|
||||
"#*/**"
|
||||
],
|
||||
":PATH:"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"useUniqueElementIds": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": "off"
|
||||
},
|
||||
"nursery": "off",
|
||||
"a11y": {
|
||||
"useSemanticElements": "off",
|
||||
"useValidAnchor": "off"
|
||||
},
|
||||
"style": {
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 4,
|
||||
"lineWidth": 120,
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
":BUN:",
|
||||
":NODE:",
|
||||
["npm:*", "npm:*/**"],
|
||||
":PACKAGE_WITH_PROTOCOL:",
|
||||
":URL:",
|
||||
":PACKAGE:",
|
||||
["/src/*", "/src/**"],
|
||||
["/**"],
|
||||
["#*", "#*/**"],
|
||||
":PATH:"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended",
|
||||
"correctness": {
|
||||
"useUniqueElementIds": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": "off"
|
||||
},
|
||||
"nursery": "off",
|
||||
"a11y": {
|
||||
"useSemanticElements": "off",
|
||||
"useValidAnchor": "off"
|
||||
},
|
||||
"style": {
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@
|
||||
"full_plugin_name": "dns-tencentcloud",
|
||||
"name": "Tencent Cloud",
|
||||
"package_name": "certbot-dns-tencentcloud",
|
||||
"version": "~=2.0.2"
|
||||
"version": "~=2.1.1"
|
||||
},
|
||||
"timeweb": {
|
||||
"credentials": "dns_timeweb_api_key = XXXXXXXXXXXXXXXXXXX",
|
||||
@@ -637,7 +637,7 @@
|
||||
"full_plugin_name": "dns-timeweb",
|
||||
"name": "Timeweb Cloud",
|
||||
"package_name": "certbot-dns-timeweb",
|
||||
"version": "~=1.0.1"
|
||||
"version": "~=2.0.0"
|
||||
},
|
||||
"transip": {
|
||||
"credentials": "dns_transip_username = my_username\ndns_transip_key_file = /etc/letsencrypt/transip-rsa.key",
|
||||
@@ -661,7 +661,7 @@
|
||||
"full_plugin_name": "dns-websupport",
|
||||
"name": "Websupport.sk",
|
||||
"package_name": "certbot-dns-websupport",
|
||||
"version": "~=2.0.1"
|
||||
"version": "~=5.0.0"
|
||||
},
|
||||
"wedos": {
|
||||
"credentials": "dns_wedos_user = <wedos_registration>\ndns_wedos_auth = <wapi_password>",
|
||||
@@ -686,5 +686,13 @@
|
||||
"name": "RcodeZero",
|
||||
"package_name": "certbot-dns-rcode0",
|
||||
"version": "~=0.0.0.2"
|
||||
},
|
||||
"lws": {
|
||||
"credentials": "dns_lws_login = 123456\ndns_lws_api_key = YOUR_API_KEY",
|
||||
"dependencies": "",
|
||||
"full_plugin_name": "dns-lws",
|
||||
"name": "LWS",
|
||||
"package_name": "certbot-dns-lws",
|
||||
"version": "~=1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import knex from "knex";
|
||||
import {configGet, configHas} from "./lib/config.js";
|
||||
import { configGet, configHas } from "./lib/config.js";
|
||||
|
||||
let instance = null;
|
||||
|
||||
@@ -23,8 +23,8 @@ const generateDbConfig = () => {
|
||||
user: cfg.user,
|
||||
password: cfg.password,
|
||||
database: cfg.name,
|
||||
port: cfg.port,
|
||||
...(cfg.ssl ? { ssl: cfg.ssl } : {})
|
||||
port: cfg.port,
|
||||
...(cfg.ssl ? { ssl: cfg.ssl } : {}),
|
||||
},
|
||||
migrations: {
|
||||
tableName: "migrations",
|
||||
@@ -37,6 +37,6 @@ const getInstance = () => {
|
||||
instance = knex(generateDbConfig());
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
|
||||
export default getInstance;
|
||||
|
||||
+7
-11
@@ -161,12 +161,12 @@ const internal2fa = {
|
||||
}
|
||||
|
||||
const result = await verify({
|
||||
token: code,
|
||||
secret: auth.meta.totp_secret,
|
||||
guardrails: createGuardrails({
|
||||
MIN_SECRET_BYTES: 10,
|
||||
}),
|
||||
});
|
||||
token: code,
|
||||
secret: auth.meta.totp_secret,
|
||||
guardrails: createGuardrails({
|
||||
MIN_SECRET_BYTES: 10,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result.valid) {
|
||||
throw new errs.AuthError("Invalid verification code");
|
||||
@@ -288,11 +288,7 @@ const internal2fa = {
|
||||
},
|
||||
|
||||
getUserPasswordAuth: async (userId) => {
|
||||
const auth = await authModel
|
||||
.query()
|
||||
.where("user_id", userId)
|
||||
.andWhere("type", "password")
|
||||
.first();
|
||||
const auth = await authModel.query().where("user_id", userId).andWhere("type", "password").first();
|
||||
|
||||
if (!auth) {
|
||||
throw new errs.ItemNotFoundError("Auth not found");
|
||||
|
||||
@@ -66,7 +66,7 @@ const internalAccessList = {
|
||||
id: data.id,
|
||||
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
|
||||
},
|
||||
true // skip masking
|
||||
true, // skip masking
|
||||
);
|
||||
|
||||
// Audit log
|
||||
@@ -180,10 +180,10 @@ const internalAccessList = {
|
||||
id: data.id,
|
||||
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
|
||||
},
|
||||
true // skip masking
|
||||
true, // skip masking
|
||||
);
|
||||
|
||||
await internalAccessList.build(freshRow)
|
||||
await internalAccessList.build(freshRow);
|
||||
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
|
||||
}
|
||||
@@ -202,17 +202,13 @@ const internalAccessList = {
|
||||
*/
|
||||
get: async (access, data, skipMasking) => {
|
||||
const thisData = data || {};
|
||||
const accessData = await access.can("access_lists:get", thisData.id)
|
||||
const accessData = await access.can("access_lists:get", thisData.id);
|
||||
|
||||
const query = accessListModel
|
||||
.query()
|
||||
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
|
||||
.leftJoin("proxy_host", function () {
|
||||
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
|
||||
"proxy_host.is_deleted",
|
||||
"=",
|
||||
0,
|
||||
);
|
||||
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
|
||||
})
|
||||
.where("access_list.is_deleted", 0)
|
||||
.andWhere("access_list.id", thisData.id)
|
||||
@@ -267,19 +263,13 @@ const internalAccessList = {
|
||||
// 4. audit log
|
||||
|
||||
// 1. update row to be deleted
|
||||
await accessListModel
|
||||
.query()
|
||||
.where("id", row.id)
|
||||
.patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
await accessListModel.query().where("id", row.id).patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
|
||||
// 2. update any proxy hosts that were using it (ignoring permissions)
|
||||
if (row.proxy_hosts) {
|
||||
await proxyHostModel
|
||||
.query()
|
||||
.where("access_list_id", "=", row.id)
|
||||
.patch({ access_list_id: 0 });
|
||||
await proxyHostModel.query().where("access_list_id", "=", row.id).patch({ access_list_id: 0 });
|
||||
|
||||
// 3. reconfigure those hosts, then reload nginx
|
||||
// set the access_list_id to zero for these items
|
||||
@@ -325,11 +315,7 @@ const internalAccessList = {
|
||||
.query()
|
||||
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
|
||||
.leftJoin("proxy_host", function () {
|
||||
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
|
||||
"proxy_host.is_deleted",
|
||||
"=",
|
||||
0,
|
||||
);
|
||||
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
|
||||
})
|
||||
.where("access_list.is_deleted", 0)
|
||||
.groupBy("access_list.id")
|
||||
@@ -371,10 +357,7 @@ const internalAccessList = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCount: async (userId, visibility) => {
|
||||
const query = accessListModel
|
||||
.query()
|
||||
.count("id as count")
|
||||
.where("is_deleted", 0);
|
||||
const query = accessListModel.query().count("id as count").where("is_deleted", 0);
|
||||
|
||||
if (visibility !== "all") {
|
||||
query.andWhere("owner_user_id", userId);
|
||||
@@ -436,20 +419,24 @@ const internalAccessList = {
|
||||
}
|
||||
|
||||
// 2. create empty access file
|
||||
fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'});
|
||||
fs.writeFileSync(htpasswdFile, "", { encoding: "utf8" });
|
||||
|
||||
// 3. generate password for each user
|
||||
if (list.items.length) {
|
||||
await new Promise((resolve, reject) => {
|
||||
batchflow(list.items).sequential()
|
||||
batchflow(list.items)
|
||||
.sequential()
|
||||
.each((_i, item, next) => {
|
||||
if (item.password?.length) {
|
||||
logger.info(`Adding: ${item.username}`);
|
||||
|
||||
utils.execFile('openssl', ['passwd', '-apr1', item.password])
|
||||
utils
|
||||
.execFile("openssl", ["passwd", "-apr1", item.password])
|
||||
.then((res) => {
|
||||
try {
|
||||
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'});
|
||||
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
@@ -471,7 +458,7 @@ const internalAccessList = {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default internalAccessList;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { castJsonIfNeed } from "../lib/helpers.js";
|
||||
import auditLogModel from "../models/audit-log.js";
|
||||
|
||||
const internalAuditLog = {
|
||||
|
||||
/**
|
||||
* All logs
|
||||
*
|
||||
@@ -46,11 +45,7 @@ const internalAuditLog = {
|
||||
get: async (access, data) => {
|
||||
await access.can("auditlog:list");
|
||||
|
||||
const query = auditLogModel
|
||||
.query()
|
||||
.andWhere("id", data.id)
|
||||
.allowGraph("[user]")
|
||||
.first();
|
||||
const query = auditLogModel.query().andWhere("id", data.id).allowGraph("[user]").first();
|
||||
|
||||
if (typeof data.expand !== "undefined" && data.expand !== null) {
|
||||
query.withGraphFetched(`[${data.expand.join(", ")}]`);
|
||||
|
||||
@@ -54,9 +54,7 @@ const internalDeadHost = {
|
||||
thisData.advanced_config = "";
|
||||
}
|
||||
|
||||
const row = await deadHostModel.query()
|
||||
.insertAndFetch(thisData)
|
||||
.then(utils.omitRow(omissions()));
|
||||
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog.add(access, {
|
||||
@@ -153,12 +151,8 @@ const internalDeadHost = {
|
||||
|
||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
||||
|
||||
|
||||
// do the row update
|
||||
await deadHostModel
|
||||
.query()
|
||||
.where({id: data.id})
|
||||
.patch(data);
|
||||
await deadHostModel.query().where({ id: data.id }).patch(data);
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog.add(access, {
|
||||
@@ -168,15 +162,18 @@ const internalDeadHost = {
|
||||
meta: thisData,
|
||||
});
|
||||
|
||||
const thisRow = await internalDeadHost
|
||||
.get(access, {
|
||||
id: thisData.id,
|
||||
expand: ["owner", "certificate"],
|
||||
});
|
||||
const thisRow = await internalDeadHost.get(access, {
|
||||
id: thisData.id,
|
||||
expand: ["owner", "certificate"],
|
||||
});
|
||||
|
||||
// Configure nginx
|
||||
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row);
|
||||
row.meta = newMeta;
|
||||
if (!thisRow.enabled) {
|
||||
// No need to add nginx config if host is disabled
|
||||
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
|
||||
}
|
||||
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", thisRow);
|
||||
thisRow.meta = newMeta;
|
||||
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
|
||||
},
|
||||
|
||||
@@ -224,18 +221,15 @@ const internalDeadHost = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
delete: async (access, data) => {
|
||||
await access.can("dead_hosts:delete", data.id)
|
||||
await access.can("dead_hosts:delete", data.id);
|
||||
const row = await internalDeadHost.get(access, { id: data.id });
|
||||
if (!row?.id) {
|
||||
throw new errs.ItemNotFoundError(data.id);
|
||||
}
|
||||
|
||||
await deadHostModel
|
||||
.query()
|
||||
.where("id", row.id)
|
||||
.patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
await deadHostModel.query().where("id", row.id).patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
|
||||
// Delete Nginx Config
|
||||
await internalNginx.deleteConfig("dead_host", row);
|
||||
@@ -259,7 +253,7 @@ const internalDeadHost = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
enable: async (access, data) => {
|
||||
await access.can("dead_hosts:update", data.id)
|
||||
await access.can("dead_hosts:update", data.id);
|
||||
const row = await internalDeadHost.get(access, {
|
||||
id: data.id,
|
||||
expand: ["certificate", "owner"],
|
||||
@@ -273,12 +267,9 @@ const internalDeadHost = {
|
||||
|
||||
row.enabled = 1;
|
||||
|
||||
await deadHostModel
|
||||
.query()
|
||||
.where("id", row.id)
|
||||
.patch({
|
||||
enabled: 1,
|
||||
});
|
||||
await deadHostModel.query().where("id", row.id).patch({
|
||||
enabled: 1,
|
||||
});
|
||||
|
||||
// Configure nginx
|
||||
await internalNginx.configure(deadHostModel, "dead_host", row);
|
||||
@@ -301,7 +292,7 @@ const internalDeadHost = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
disable: async (access, data) => {
|
||||
await access.can("dead_hosts:update", data.id)
|
||||
await access.can("dead_hosts:update", data.id);
|
||||
const row = await internalDeadHost.get(access, { id: data.id });
|
||||
if (!row?.id) {
|
||||
throw new errs.ItemNotFoundError(data.id);
|
||||
@@ -312,12 +303,9 @@ const internalDeadHost = {
|
||||
|
||||
row.enabled = 0;
|
||||
|
||||
await deadHostModel
|
||||
.query()
|
||||
.where("id", row.id)
|
||||
.patch({
|
||||
enabled: 0,
|
||||
});
|
||||
await deadHostModel.query().where("id", row.id).patch({
|
||||
enabled: 0,
|
||||
});
|
||||
|
||||
// Delete Nginx Config
|
||||
await internalNginx.deleteConfig("dead_host", row);
|
||||
@@ -342,7 +330,7 @@ const internalDeadHost = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAll: async (access, expand, searchQuery) => {
|
||||
const accessData = await access.can("dead_hosts:list")
|
||||
const accessData = await access.can("dead_hosts:list");
|
||||
const query = deadHostModel
|
||||
.query()
|
||||
.where("is_deleted", 0)
|
||||
|
||||
@@ -217,7 +217,10 @@ const internalNginx = {
|
||||
}
|
||||
|
||||
// For redirection hosts, if the scheme is not http or https, set it to $scheme
|
||||
if (nice_host_type === "redirection_host" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) {
|
||||
if (
|
||||
nice_host_type === "redirection_host" &&
|
||||
["http", "https"].indexOf(host.forward_scheme.toLowerCase()) === -1
|
||||
) {
|
||||
host.forward_scheme = "$scheme";
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,7 @@ export default {
|
||||
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
|
||||
}
|
||||
|
||||
const auth = await authModel
|
||||
.query()
|
||||
.where("user_id", "=", user.id)
|
||||
.where("type", "=", "password")
|
||||
.first();
|
||||
const auth = await authModel.query().where("user_id", "=", user.id).where("type", "=", "password").first();
|
||||
|
||||
if (!auth) {
|
||||
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
|
||||
@@ -50,10 +46,7 @@ export default {
|
||||
|
||||
const valid = await auth.verifyPassword(data.secret);
|
||||
if (!valid) {
|
||||
throw new errs.AuthError(
|
||||
ERROR_MESSAGE_INVALID_AUTH,
|
||||
ERROR_MESSAGE_INVALID_AUTH_I18N,
|
||||
);
|
||||
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH, ERROR_MESSAGE_INVALID_AUTH_I18N);
|
||||
}
|
||||
|
||||
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
|
||||
@@ -171,7 +164,7 @@ export default {
|
||||
}
|
||||
|
||||
// Check scope
|
||||
if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") {
|
||||
if (tokenData.scope?.[0] !== "2fa-challenge") {
|
||||
throw new errs.AuthError("Invalid challenge token");
|
||||
}
|
||||
|
||||
@@ -183,10 +176,7 @@ export default {
|
||||
// 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,
|
||||
);
|
||||
throw new errs.AuthError(ERROR_MESSAGE_INVALID_2FA, ERROR_MESSAGE_INVALID_2FA_I18N);
|
||||
}
|
||||
|
||||
// Create full token
|
||||
|
||||
@@ -257,11 +257,9 @@ const internalUser = {
|
||||
},
|
||||
|
||||
deleteAll: async () => {
|
||||
await userModel
|
||||
.query()
|
||||
.patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
await userModel.query().patch({
|
||||
is_deleted: 1,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+11
-11
@@ -1,19 +1,19 @@
|
||||
module.exports = {
|
||||
development: {
|
||||
client: 'mysql2',
|
||||
client: "mysql2",
|
||||
migrations: {
|
||||
tableName: 'migrations',
|
||||
stub: 'lib/migrate_template.js',
|
||||
directory: 'migrations'
|
||||
}
|
||||
tableName: "migrations",
|
||||
stub: "lib/migrate_template.js",
|
||||
directory: "migrations",
|
||||
},
|
||||
},
|
||||
|
||||
production: {
|
||||
client: 'mysql2',
|
||||
client: "mysql2",
|
||||
migrations: {
|
||||
tableName: 'migrations',
|
||||
stub: 'lib/migrate_template.js',
|
||||
directory: 'migrations'
|
||||
}
|
||||
}
|
||||
tableName: "migrations",
|
||||
stub: "lib/migrate_template.js",
|
||||
directory: "migrations",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -119,10 +119,7 @@ export default function (tokenString) {
|
||||
|
||||
// Proxy Hosts
|
||||
case "proxy_hosts": {
|
||||
const query = proxyHostModel
|
||||
.query()
|
||||
.select("id")
|
||||
.andWhere("is_deleted", 0);
|
||||
const query = proxyHostModel.query().select("id").andWhere("is_deleted", 0);
|
||||
|
||||
if (permissions.visibility === "user") {
|
||||
query.andWhere("owner_user_id", tokenUserId);
|
||||
@@ -271,7 +268,7 @@ export default function (tokenString) {
|
||||
err.permission = permission;
|
||||
err.permission_data = data;
|
||||
logger.error(permission, data, err.message);
|
||||
throw errs.PermissionError("Permission Denied", err);
|
||||
throw new errs.PermissionError("Permission Denied", err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+37
-17
@@ -2,13 +2,13 @@ import fs from "node:fs";
|
||||
import NodeRSA from "node-rsa";
|
||||
import { global as logger } from "../logger.js";
|
||||
|
||||
const keysFile = '/data/keys.json';
|
||||
const mysqlEngine = 'mysql2';
|
||||
const postgresEngine = 'pg';
|
||||
const sqliteClientName = 'better-sqlite3';
|
||||
const keysFile = "/data/keys.json";
|
||||
const mysqlEngine = "mysql2";
|
||||
const postgresEngine = "pg";
|
||||
const sqliteClientName = "better-sqlite3";
|
||||
|
||||
// Not used for new setups anymore but may exist in legacy setups
|
||||
const legacySqliteClientName = 'sqlite3';
|
||||
const legacySqliteClientName = "sqlite3";
|
||||
|
||||
let instance = null;
|
||||
|
||||
@@ -40,14 +40,20 @@ const configure = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const toBool = (v) => /^(1|true|yes|on)$/i.test((v || '').trim());
|
||||
const toBool = (v) => /^(1|true|yes|on)$/i.test((v || "").trim());
|
||||
|
||||
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
|
||||
const envMysqlUser = process.env.DB_MYSQL_USER || null;
|
||||
const envMysqlName = process.env.DB_MYSQL_NAME || null;
|
||||
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL);
|
||||
const envMysqlSSLRejectUnauthorized = process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined ? true : toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
|
||||
const envMysqlSSLVerifyIdentity = process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
|
||||
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
|
||||
const envMysqlUser = process.env.DB_MYSQL_USER || null;
|
||||
const envMysqlName = process.env.DB_MYSQL_NAME || null;
|
||||
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL);
|
||||
const envMysqlSSLRejectUnauthorized =
|
||||
process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined
|
||||
? true
|
||||
: toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
|
||||
const envMysqlSSLVerifyIdentity =
|
||||
process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined
|
||||
? true
|
||||
: toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
|
||||
if (envMysqlHost && envMysqlUser && envMysqlName) {
|
||||
// we have enough mysql creds to go with mysql
|
||||
logger.info("Using MySQL configuration");
|
||||
@@ -58,8 +64,10 @@ const configure = () => {
|
||||
port: process.env.DB_MYSQL_PORT || 3306,
|
||||
user: envMysqlUser,
|
||||
password: process.env.DB_MYSQL_PASSWORD,
|
||||
name: envMysqlName,
|
||||
ssl: envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false,
|
||||
name: envMysqlName,
|
||||
ssl: envMysqlSSL
|
||||
? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity }
|
||||
: false,
|
||||
},
|
||||
keys: getKeys(),
|
||||
};
|
||||
@@ -137,7 +145,7 @@ const generateKeys = () => {
|
||||
|
||||
// Write keys config
|
||||
try {
|
||||
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2));
|
||||
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2), { mode: 0o600 });
|
||||
} catch (err) {
|
||||
logger.error(`Could not write JWT key pair to config file: ${keysFile}: ${err.message}`);
|
||||
process.exit(1);
|
||||
@@ -222,7 +230,7 @@ const isDebugMode = () => !!process.env.DEBUG;
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isCI = () => process.env.CI === 'true' && process.env.DEBUG === 'true';
|
||||
const isCI = () => process.env.CI === "true" && process.env.DEBUG === "true";
|
||||
|
||||
/**
|
||||
* Returns a public key
|
||||
@@ -259,4 +267,16 @@ const useLetsencryptServer = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer };
|
||||
export {
|
||||
isCI,
|
||||
configHas,
|
||||
configGet,
|
||||
isSqlite,
|
||||
isMysql,
|
||||
isPostgres,
|
||||
isDebugMode,
|
||||
getPrivateKey,
|
||||
getPublicKey,
|
||||
useLetsencryptStaging,
|
||||
useLetsencryptServer,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from "lodash";
|
||||
import _ from "lodash";
|
||||
|
||||
export default (default_sort, default_offset, default_limit, max_limit) => {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default (req, res, next) => {
|
||||
if (req.params.user_id === 'me' && res.locals.access) {
|
||||
req.params.user_id = res.locals.access.token.get('attrs').id;
|
||||
if (req.params.user_id === "me" && res.locals.access) {
|
||||
req.params.user_id = res.locals.access.token.get("attrs").id;
|
||||
} else {
|
||||
req.params.user_id = Number.parseInt(req.params.user_id, 10);
|
||||
}
|
||||
|
||||
@@ -24,21 +24,17 @@ const apiValidator = async (schema, payload /*, description*/) => {
|
||||
throw new errs.ValidationError("Payload is undefined");
|
||||
}
|
||||
|
||||
|
||||
const validate = ajv.compile(schema);
|
||||
|
||||
const valid = validate(payload);
|
||||
|
||||
|
||||
if (valid && !validate.errors) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const message = ajv.errorsText(validate.errors);
|
||||
const err = new errs.ValidationError(message);
|
||||
err.debug = {validationErrors: validate.errors, payload};
|
||||
err.debug = { validationErrors: validate.errors, payload };
|
||||
throw err;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv from 'ajv/dist/2020.js';
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import _ from "lodash";
|
||||
import commonDefinitions from "../../schema/common.json" with { type: "json" };
|
||||
import errs from "../error.js";
|
||||
|
||||
@@ -13,13 +13,14 @@ const migrateName = "settings";
|
||||
const up = (knex) => {
|
||||
logger.info(`[${migrateName}] Migrating Up...`);
|
||||
|
||||
return knex.schema.createTable('setting', (table) => {
|
||||
table.string('id').notNull().primary();
|
||||
table.string('name', 100).notNull();
|
||||
table.string('description', 255).notNull();
|
||||
table.string('value', 255).notNull();
|
||||
table.json('meta').notNull();
|
||||
})
|
||||
return knex.schema
|
||||
.createTable("setting", (table) => {
|
||||
table.string("id").notNull().primary();
|
||||
table.string("name", 100).notNull();
|
||||
table.string("description", 255).notNull();
|
||||
table.string("value", 255).notNull();
|
||||
table.json("meta").notNull();
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrateName}] setting Table created`);
|
||||
});
|
||||
|
||||
@@ -17,9 +17,7 @@ const up = (knex) => {
|
||||
.table("redirection_host", async (table) => {
|
||||
// change the column default from $scheme to auto
|
||||
await table.string("forward_scheme").notNull().defaultTo("auto").alter();
|
||||
await knex('redirection_host')
|
||||
.where('forward_scheme', '$scheme')
|
||||
.update({ forward_scheme: 'auto' });
|
||||
await knex("redirection_host").where("forward_scheme", "$scheme").update({ forward_scheme: "auto" });
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
||||
@@ -38,9 +36,7 @@ const down = (knex) => {
|
||||
return knex.schema
|
||||
.table("redirection_host", async (table) => {
|
||||
await table.string("forward_scheme").notNull().defaultTo("$scheme").alter();
|
||||
await knex('redirection_host')
|
||||
.where('forward_scheme', 'auto')
|
||||
.update({ forward_scheme: '$scheme' });
|
||||
await knex("redirection_host").where("forward_scheme", "auto").update({ forward_scheme: "$scheme" });
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
||||
|
||||
@@ -11,15 +11,15 @@ const migrateName = "trust_forwarded_proto";
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const up = (knex) => {
|
||||
logger.info(`[${migrateName}] Migrating Up...`);
|
||||
logger.info(`[${migrateName}] Migrating Up...`);
|
||||
|
||||
return knex.schema
|
||||
.alterTable('proxy_host', (table) => {
|
||||
table.tinyint('trust_forwarded_proto').notNullable().defaultTo(0);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
||||
});
|
||||
return knex.schema
|
||||
.alterTable("proxy_host", (table) => {
|
||||
table.tinyint("trust_forwarded_proto").notNullable().defaultTo(0);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -29,15 +29,15 @@ const up = (knex) => {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const down = (knex) => {
|
||||
logger.info(`[${migrateName}] Migrating Down...`);
|
||||
logger.info(`[${migrateName}] Migrating Down...`);
|
||||
|
||||
return knex.schema
|
||||
.alterTable('proxy_host', (table) => {
|
||||
table.dropColumn('trust_forwarded_proto');
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
||||
});
|
||||
return knex.schema
|
||||
.alterTable("proxy_host", (table) => {
|
||||
table.dropColumn("trust_forwarded_proto");
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
||||
});
|
||||
};
|
||||
|
||||
export { up, down };
|
||||
export { up, down };
|
||||
|
||||
@@ -7,23 +7,23 @@ import db from "../db.js";
|
||||
Model.knex(db());
|
||||
|
||||
class Setting extends Model {
|
||||
$beforeInsert () {
|
||||
$beforeInsert() {
|
||||
// Default for meta
|
||||
if (typeof this.meta === 'undefined') {
|
||||
if (typeof this.meta === "undefined") {
|
||||
this.meta = {};
|
||||
}
|
||||
}
|
||||
|
||||
static get name () {
|
||||
return 'Setting';
|
||||
static get name() {
|
||||
return "Setting";
|
||||
}
|
||||
|
||||
static get tableName () {
|
||||
return 'setting';
|
||||
static get tableName() {
|
||||
return "setting";
|
||||
}
|
||||
|
||||
static get jsonAttributes () {
|
||||
return ['meta'];
|
||||
static get jsonAttributes() {
|
||||
return ["meta"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,21 +8,21 @@ import now from "./now_helper.js";
|
||||
Model.knex(db());
|
||||
|
||||
class UserPermission extends Model {
|
||||
$beforeInsert () {
|
||||
this.created_on = now();
|
||||
$beforeInsert() {
|
||||
this.created_on = now();
|
||||
this.modified_on = now();
|
||||
}
|
||||
|
||||
$beforeUpdate () {
|
||||
$beforeUpdate() {
|
||||
this.modified_on = now();
|
||||
}
|
||||
|
||||
static get name () {
|
||||
return 'UserPermission';
|
||||
static get name() {
|
||||
return "UserPermission";
|
||||
}
|
||||
|
||||
static get tableName () {
|
||||
return 'user_permission';
|
||||
static get tableName() {
|
||||
return "user_permission";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -13,37 +13,37 @@
|
||||
"regenerate-config": "node scripts/regenerate-config"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^15.3.5",
|
||||
"@apidevtools/json-schema-ref-parser": "^16.0.0",
|
||||
"ajv": "^8.20.0",
|
||||
"archiver": "^8.0.0",
|
||||
"batchflow": "^0.4.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.10.0",
|
||||
"body-parser": "^2.2.2",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"body-parser": "^2.3.0",
|
||||
"chalk": "5.6.2",
|
||||
"compression": "^1.8.1",
|
||||
"express": "^5.2.1",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"gravatar": "^1.8.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"knex": "3.2.10",
|
||||
"liquidjs": "10.27.0",
|
||||
"knex": "3.3.0",
|
||||
"liquidjs": "10.29.0",
|
||||
"lodash": "^4.18.1",
|
||||
"moment": "^2.30.1",
|
||||
"mysql2": "^3.22.3",
|
||||
"mysql2": "^3.23.4",
|
||||
"node-rsa": "^2.0.0",
|
||||
"objection": "3.1.5",
|
||||
"otplib": "^13.4.0",
|
||||
"otplib": "^13.5.0",
|
||||
"path": "^0.12.7",
|
||||
"pg": "^8.21.0",
|
||||
"proxy-agent": "^8.0.1",
|
||||
"pg": "^8.23.0",
|
||||
"proxy-agent": "^8.0.2",
|
||||
"signale": "1.4.0",
|
||||
"sqlite3": "^6.0.1",
|
||||
"temp-write": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-parser": "^12.1.0",
|
||||
"@biomejs/biome": "^2.4.15",
|
||||
"@biomejs/biome": "^2.5.10",
|
||||
"nodemon": "^3.1.14"
|
||||
},
|
||||
"signale": {
|
||||
|
||||
@@ -86,10 +86,7 @@ router
|
||||
},
|
||||
{
|
||||
event_id: req.params.event_id,
|
||||
expand:
|
||||
typeof req.query.expand === "string"
|
||||
? req.query.expand.split(",")
|
||||
: null,
|
||||
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -44,18 +44,11 @@ router
|
||||
},
|
||||
},
|
||||
{
|
||||
expand:
|
||||
typeof req.query.expand === "string"
|
||||
? req.query.expand.split(",")
|
||||
: null,
|
||||
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
|
||||
query: typeof req.query.query === "string" ? req.query.query : null,
|
||||
},
|
||||
);
|
||||
const rows = await internalCertificate.getAll(
|
||||
res.locals.access,
|
||||
data.expand,
|
||||
data.query,
|
||||
);
|
||||
const rows = await internalCertificate.getAll(res.locals.access, data.expand, data.query);
|
||||
res.status(200).send(rows);
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
@@ -70,15 +63,9 @@ router
|
||||
*/
|
||||
.post(async (req, res, next) => {
|
||||
try {
|
||||
const payload = await apiValidator(
|
||||
getValidationSchema("/nginx/certificates", "post"),
|
||||
req.body,
|
||||
);
|
||||
const payload = await apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body);
|
||||
req.setTimeout(900000); // 15 minutes timeout
|
||||
const result = await internalCertificate.create(
|
||||
res.locals.access,
|
||||
payload,
|
||||
);
|
||||
const result = await internalCertificate.create(res.locals.access, payload);
|
||||
res.status(201).send(result);
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
@@ -139,16 +126,10 @@ router
|
||||
*/
|
||||
.post(async (req, res, next) => {
|
||||
try {
|
||||
const payload = await apiValidator(
|
||||
getValidationSchema("/nginx/certificates/test-http", "post"),
|
||||
req.body,
|
||||
);
|
||||
const payload = await apiValidator(getValidationSchema("/nginx/certificates/test-http", "post"), req.body);
|
||||
req.setTimeout(60000); // 1 minute timeout
|
||||
|
||||
const result = await internalCertificate.testHttpsChallenge(
|
||||
res.locals.access,
|
||||
payload,
|
||||
);
|
||||
const result = await internalCertificate.testHttpsChallenge(res.locals.access, payload);
|
||||
res.status(200).send(result);
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
@@ -224,10 +205,7 @@ router
|
||||
},
|
||||
{
|
||||
certificate_id: req.params.certificate_id,
|
||||
expand:
|
||||
typeof req.query.expand === "string"
|
||||
? req.query.expand.split(",")
|
||||
: null,
|
||||
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
|
||||
},
|
||||
);
|
||||
const row = await internalCertificate.get(res.locals.access, {
|
||||
|
||||
@@ -194,9 +194,11 @@ router
|
||||
/**
|
||||
* POST /api/nginx/dead-hosts/123/disable
|
||||
*/
|
||||
.post((req, res, next) => {
|
||||
.post(async (req, res, next) => {
|
||||
try {
|
||||
const result = internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) });
|
||||
const result = await internalDeadHost.disable(res.locals.access, {
|
||||
id: Number.parseInt(req.params.host_id, 10),
|
||||
});
|
||||
res.status(200).send(result);
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
|
||||
+10
-41
@@ -48,18 +48,11 @@ router
|
||||
},
|
||||
},
|
||||
{
|
||||
expand:
|
||||
typeof req.query.expand === "string"
|
||||
? req.query.expand.split(",")
|
||||
: null,
|
||||
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
|
||||
query: typeof req.query.query === "string" ? req.query.query : null,
|
||||
},
|
||||
);
|
||||
const users = await internalUser.getAll(
|
||||
res.locals.access,
|
||||
data.expand,
|
||||
data.query,
|
||||
);
|
||||
const users = await internalUser.getAll(res.locals.access, data.expand, data.query);
|
||||
res.status(200).send(users);
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
@@ -95,10 +88,7 @@ router
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await apiValidator(
|
||||
getValidationSchema("/users", "post"),
|
||||
body,
|
||||
);
|
||||
const payload = await apiValidator(getValidationSchema("/users", "post"), body);
|
||||
const user = await internalUser.create(res.locals.access, payload);
|
||||
res.status(201).send(user);
|
||||
} catch (err) {
|
||||
@@ -169,20 +159,14 @@ router
|
||||
},
|
||||
{
|
||||
user_id: req.params.user_id,
|
||||
expand:
|
||||
typeof req.query.expand === "string"
|
||||
? req.query.expand.split(",")
|
||||
: null,
|
||||
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
|
||||
},
|
||||
);
|
||||
|
||||
const user = await internalUser.get(res.locals.access, {
|
||||
id: data.user_id,
|
||||
expand: data.expand,
|
||||
omit: internalUser.getUserOmisionsByAccess(
|
||||
res.locals.access,
|
||||
data.user_id,
|
||||
),
|
||||
omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id),
|
||||
});
|
||||
res.status(200).send(user);
|
||||
} catch (err) {
|
||||
@@ -198,10 +182,7 @@ router
|
||||
*/
|
||||
.put(async (req, res, next) => {
|
||||
try {
|
||||
const payload = await apiValidator(
|
||||
getValidationSchema("/users/{userID}", "put"),
|
||||
req.body,
|
||||
);
|
||||
const payload = await apiValidator(getValidationSchema("/users/{userID}", "put"), req.body);
|
||||
payload.id = req.params.user_id;
|
||||
const result = await internalUser.update(res.locals.access, payload);
|
||||
res.status(200).send(result);
|
||||
@@ -248,10 +229,7 @@ router
|
||||
*/
|
||||
.put(async (req, res, next) => {
|
||||
try {
|
||||
const payload = await apiValidator(
|
||||
getValidationSchema("/users/{userID}/auth", "put"),
|
||||
req.body,
|
||||
);
|
||||
const payload = await apiValidator(getValidationSchema("/users/{userID}/auth", "put"), req.body);
|
||||
payload.id = req.params.user_id;
|
||||
const result = await internalUser.setPassword(res.locals.access, payload);
|
||||
res.status(200).send(result);
|
||||
@@ -281,15 +259,9 @@ router
|
||||
*/
|
||||
.put(async (req, res, next) => {
|
||||
try {
|
||||
const payload = await apiValidator(
|
||||
getValidationSchema("/users/{userID}/permissions", "put"),
|
||||
req.body,
|
||||
);
|
||||
const payload = await apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body);
|
||||
payload.id = req.params.user_id;
|
||||
const result = await internalUser.setPermissions(
|
||||
res.locals.access,
|
||||
payload,
|
||||
);
|
||||
const result = await internalUser.setPermissions(res.locals.access, payload);
|
||||
res.status(200).send(result);
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
@@ -408,10 +380,7 @@ router
|
||||
*/
|
||||
.post(async (req, res, next) => {
|
||||
try {
|
||||
const { code } = await apiValidator(
|
||||
getValidationSchema("/users/{userID}/2fa/enable", "post"),
|
||||
req.body,
|
||||
);
|
||||
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) {
|
||||
|
||||
+32
-35
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { installPlugins } from "./lib/certbot.js";
|
||||
import utils from "./lib/utils.js";
|
||||
import { setup as logger } from "./logger.js";
|
||||
@@ -6,12 +7,11 @@ import certificateModel from "./models/certificate.js";
|
||||
import settingModel from "./models/setting.js";
|
||||
import userModel from "./models/user.js";
|
||||
import userPermissionModel from "./models/user_permission.js";
|
||||
import fs from "fs/promises";
|
||||
|
||||
export const isSetup = async () => {
|
||||
const row = await userModel.query().select("id").where("is_deleted", 0).first();
|
||||
return row?.id > 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a default admin users if one doesn't already exist in the database
|
||||
@@ -45,18 +45,14 @@ const setupDefaultUser = async () => {
|
||||
roles: ["admin"],
|
||||
};
|
||||
|
||||
const user = await userModel
|
||||
.query()
|
||||
.insertAndFetch(data);
|
||||
const user = await userModel.query().insertAndFetch(data);
|
||||
|
||||
await authModel
|
||||
.query()
|
||||
.insert({
|
||||
user_id: user.id,
|
||||
type: "password",
|
||||
secret: initialAdminPassword,
|
||||
meta: {},
|
||||
});
|
||||
await authModel.query().insert({
|
||||
user_id: user.id,
|
||||
type: "password",
|
||||
secret: initialAdminPassword,
|
||||
meta: {},
|
||||
});
|
||||
|
||||
await userPermissionModel.query().insert({
|
||||
user_id: user.id,
|
||||
@@ -78,22 +74,16 @@ const setupDefaultUser = async () => {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const setupDefaultSettings = async () => {
|
||||
const row = await settingModel
|
||||
.query()
|
||||
.select("id")
|
||||
.where({ id: "default-site" })
|
||||
.first();
|
||||
const row = await settingModel.query().select("id").where({ id: "default-site" }).first();
|
||||
|
||||
if (!row?.id) {
|
||||
await settingModel
|
||||
.query()
|
||||
.insert({
|
||||
id: "default-site",
|
||||
name: "Default Site",
|
||||
description: "What to show when Nginx is hit with an unknown Host",
|
||||
value: "congratulations",
|
||||
meta: {},
|
||||
});
|
||||
await settingModel.query().insert({
|
||||
id: "default-site",
|
||||
name: "Default Site",
|
||||
description: "What to show when Nginx is hit with an unknown Host",
|
||||
value: "congratulations",
|
||||
meta: {},
|
||||
});
|
||||
logger.info("Default settings added");
|
||||
}
|
||||
};
|
||||
@@ -104,10 +94,7 @@ const setupDefaultSettings = async () => {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const setupCertbotPlugins = async () => {
|
||||
const certificates = await certificateModel
|
||||
.query()
|
||||
.where("is_deleted", 0)
|
||||
.andWhere("provider", "letsencrypt");
|
||||
const certificates = await certificateModel.query().where("is_deleted", 0).andWhere("provider", "letsencrypt");
|
||||
|
||||
if (certificates?.length) {
|
||||
const plugins = [];
|
||||
@@ -122,14 +109,24 @@ const setupCertbotPlugins = async () => {
|
||||
// Make sure credentials file exists
|
||||
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
|
||||
if (typeof certificate.meta.dns_provider_credentials === "string") {
|
||||
promises.push(fs.mkdir("/etc/letsencrypt/credentials", { recursive: true })
|
||||
.then(() => fs.writeFile(credentials_loc, certificate.meta.dns_provider_credentials, { mode: 0o600, flag: "wx" }))
|
||||
.catch((err) => { if (err.code !== "EEXIST") throw err; }));
|
||||
promises.push(
|
||||
fs
|
||||
.mkdir("/etc/letsencrypt/credentials", { recursive: true })
|
||||
.then(() =>
|
||||
fs.writeFile(credentials_loc, certificate.meta.dns_provider_credentials, {
|
||||
mode: 0o600,
|
||||
flag: "wx",
|
||||
}),
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err.code !== "EEXIST") throw err;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
await installPlugins(plugins);
|
||||
|
||||
if (promises.length) {
|
||||
|
||||
+386
-339
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,12 @@ for loc in "${locations[@]}"; do
|
||||
chownit "$loc"
|
||||
done
|
||||
|
||||
# Ensure the JWT key file is owned by the runtime user, even when the /data
|
||||
# directory ownership already matches PUID:PGID (chownit skips recursion then)
|
||||
if [ -f /data/keys.json ]; then
|
||||
chown "$PUID:$PGID" /data/keys.json
|
||||
fi
|
||||
|
||||
if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then
|
||||
log_info 'Skipping ownership change of certbot directories'
|
||||
else
|
||||
|
||||
+73
-90
@@ -1,92 +1,75 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"!**/dist/**/*"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 4,
|
||||
"lineWidth": 120,
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
":BUN:",
|
||||
":NODE:",
|
||||
[
|
||||
"npm:*",
|
||||
"npm:*/**"
|
||||
],
|
||||
":PACKAGE_WITH_PROTOCOL:",
|
||||
":URL:",
|
||||
":PACKAGE:",
|
||||
[
|
||||
"/src/*",
|
||||
"/src/**"
|
||||
],
|
||||
[
|
||||
"/**"
|
||||
],
|
||||
[
|
||||
"#*",
|
||||
"#*/**"
|
||||
],
|
||||
":PATH:"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"useUniqueElementIds": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": "off"
|
||||
},
|
||||
"nursery": "off",
|
||||
"a11y": {
|
||||
"useSemanticElements": "off",
|
||||
"useValidAnchor": "off"
|
||||
},
|
||||
"style": {
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 4,
|
||||
"lineWidth": 120,
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
":BUN:",
|
||||
":NODE:",
|
||||
["npm:*", "npm:*/**"],
|
||||
":PACKAGE_WITH_PROTOCOL:",
|
||||
":URL:",
|
||||
":PACKAGE:",
|
||||
["/src/*", "/src/**"],
|
||||
["/**"],
|
||||
["#*", "#*/**"],
|
||||
":PATH:"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended",
|
||||
"correctness": {
|
||||
"useUniqueElementIds": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": "off"
|
||||
},
|
||||
"nursery": "off",
|
||||
"a11y": {
|
||||
"useSemanticElements": "off",
|
||||
"useValidAnchor": "off"
|
||||
},
|
||||
"style": {
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const allLocales = [
|
||||
["tr", "tr-TR"],
|
||||
["hu", "hu-HU"],
|
||||
["no", "no-NO"],
|
||||
["uk", "uk-UA"],
|
||||
];
|
||||
|
||||
const ignoreUnused = [/^.*$/];
|
||||
|
||||
+26
-25
@@ -17,50 +17,51 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/core": "^1.4.0",
|
||||
"@tabler/icons-react": "^3.44.0",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tabler/icons-react": "^3.46.0",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-table": "^9.1.2",
|
||||
"@uiw/react-textarea-code-editor": "^3.1.1",
|
||||
"classnames": "^2.5.1",
|
||||
"country-flag-icons": "^1.6.17",
|
||||
"country-flag-icons": "^1.6.20",
|
||||
"date-fns": "^4.4.0",
|
||||
"ez-modal-react": "^1.0.5",
|
||||
"formik": "^2.4.9",
|
||||
"generate-password-browser": "^1.1.0",
|
||||
"humps": "^2.0.1",
|
||||
"query-string": "^9.4.0",
|
||||
"react": "^19.2.7",
|
||||
"query-string": "^9.5.0",
|
||||
"react": "^19.2.8",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-intl": "^10.1.11",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-intl": "^10.1.22",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.16.0",
|
||||
"react-qr-code": "^2.2.0",
|
||||
"react-router-dom": "^7.18.2",
|
||||
"react-select": "^5.10.2",
|
||||
"react-toastify": "^11.1.0",
|
||||
"rooks": "^9.8.0"
|
||||
"rooks": "^9.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.15",
|
||||
"@formatjs/cli": "^6.16.6",
|
||||
"@tanstack/react-query-devtools": "^5.100.14",
|
||||
"@biomejs/biome": "^2.5.10",
|
||||
"@formatjs/cli": "^6.16.19",
|
||||
"@tanstack/react-query-devtools": "^5.101.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/country-flag-icons": "^1.2.2",
|
||||
"@types/humps": "^2.0.6",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/react": "^19.2.16",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"happy-dom": "^20.9.0",
|
||||
"postcss": "^8.5.15",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"happy-dom": "^20.11.6",
|
||||
"postcss": "^8.5.26",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass": "^1.100.0",
|
||||
"sass": "^1.103.1",
|
||||
"tmp": "^0.2.7",
|
||||
"typescript": "6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-checker": "^0.14.1",
|
||||
"vitest": "^4.1.8"
|
||||
"typescript": "7.0.2",
|
||||
"vite": "^8.2.2",
|
||||
"vite-plugin-checker": "^0.14.5",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Table as ReactTable } from "@tanstack/react-table";
|
||||
import type { Table as ReactTable, RowData } from "@tanstack/react-table";
|
||||
import cn from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button, HasPermission } from "src/components";
|
||||
import type { Features } from "src/components/Table/features";
|
||||
import { T } from "src/locale";
|
||||
import { type ADMIN, MANAGE, type Permission, type Section } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
tableInstance: ReactTable<any>;
|
||||
interface Props<TData extends RowData> {
|
||||
tableInstance: ReactTable<Features, TData>;
|
||||
onNew?: () => void;
|
||||
isFiltered?: boolean;
|
||||
object: string;
|
||||
@@ -16,7 +17,7 @@ interface Props {
|
||||
permissionSection?: Section | typeof ADMIN;
|
||||
permission?: Permission;
|
||||
}
|
||||
function EmptyData({
|
||||
function EmptyData<TData extends RowData>({
|
||||
tableInstance,
|
||||
onNew,
|
||||
isFiltered,
|
||||
@@ -26,7 +27,7 @@ function EmptyData({
|
||||
customAddBtn,
|
||||
permissionSection,
|
||||
permission,
|
||||
}: Props) {
|
||||
}: Props<TData>) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
|
||||
|
||||
@@ -65,8 +65,12 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
|
||||
value={client.directive}
|
||||
onChange={(e) => handleChange(idx, "directive", e.target.value)}
|
||||
>
|
||||
<option value="allow"><T id="action.allow" /></option>
|
||||
<option value="deny"><T id="action.deny" /></option>
|
||||
<option value="allow">
|
||||
<T id="action.allow" />
|
||||
</option>
|
||||
<option value="deny">
|
||||
<T id="action.deny" />
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<input
|
||||
@@ -81,16 +85,13 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-1">
|
||||
<a
|
||||
role="button"
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-danger p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemove(idx);
|
||||
}}
|
||||
onClick={() => handleRemove(idx)}
|
||||
>
|
||||
<IconX size={16} />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -112,7 +113,9 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
|
||||
value="deny"
|
||||
disabled
|
||||
>
|
||||
<option value="deny"><T id="action.deny" /></option>
|
||||
<option value="deny">
|
||||
<T id="action.deny" />
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<input
|
||||
|
||||
@@ -82,16 +82,13 @@ export function BasicAuthFields({ initialValues, name = "items" }: Props) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-1">
|
||||
<a
|
||||
role="button"
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-danger p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemove(idx);
|
||||
}}
|
||||
onClick={() => handleRemove(idx)}
|
||||
>
|
||||
<IconX size={16} />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
/* card-active points --tblr-card-border-color at --tblr-primary, which both the
|
||||
card outline and the card header's bottom border are drawn from. Tabler's
|
||||
stylesheet is loaded after this one, so the override needs !important. */
|
||||
.locationCard {
|
||||
border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;
|
||||
--tblr-card-border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;
|
||||
}
|
||||
|
||||
.filter {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
/* The header is a plain toggle rather than a button-styled control, so that a
|
||||
list of collapsed locations reads as rows instead of a stack of buttons. */
|
||||
.toggle {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.toggle:focus-visible {
|
||||
outline: 2px solid var(--tblr-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Keeps the marker on one line next to the delete button, and lets it drop out
|
||||
of the way before the path does when the row runs out of room. */
|
||||
.marker {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.path {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.summary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
import { IconSettings } from "@tabler/icons-react";
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconTrash,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import CodeEditor from "@uiw/react-textarea-code-editor";
|
||||
import cn from "classnames";
|
||||
import { useFormikContext } from "formik";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import type { ProxyLocation } from "src/api/backend";
|
||||
import { intl, T } from "src/locale";
|
||||
import styles from "./LocationsFields.module.css";
|
||||
|
||||
// Below this many locations the list is short enough to scan by eye, and the
|
||||
// filter would only take up space.
|
||||
const FILTER_THRESHOLD = 5;
|
||||
|
||||
// Locations are identified by a client-side id rather than their array index,
|
||||
// so that expanded/advanced state stays with the right row when one is removed.
|
||||
interface Row {
|
||||
id: number;
|
||||
value: ProxyLocation;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
initialValues: ProxyLocation[];
|
||||
name?: string;
|
||||
}
|
||||
export function LocationsFields({ initialValues, name = "locations" }: Props) {
|
||||
const [values, setValues] = useState<ProxyLocation[]>(initialValues || []);
|
||||
const [rows, setRows] = useState<Row[]>(() => (initialValues || []).map((value, id) => ({ id, value })));
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [expanded, setExpanded] = useState<number[]>([]);
|
||||
const [advVisible, setAdvVisible] = useState<number[]>([]);
|
||||
const [filter, setFilter] = useState("");
|
||||
const nextId = useRef(rows.length);
|
||||
const scrollToId = useRef<number | null>(null);
|
||||
|
||||
const blankItem: ProxyLocation = {
|
||||
path: "",
|
||||
@@ -24,32 +47,62 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) {
|
||||
forwardPort: 80,
|
||||
};
|
||||
|
||||
const toggleAdvVisible = (idx: number) => {
|
||||
setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]);
|
||||
const toggleExpanded = (id: number) => {
|
||||
setExpanded(expanded.includes(id) ? expanded.filter((i) => i !== id) : [...expanded, id]);
|
||||
};
|
||||
|
||||
const toggleAdvVisible = (id: number) => {
|
||||
setAdvVisible(advVisible.includes(id) ? advVisible.filter((i) => i !== id) : [...advVisible, id]);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setValues([...values, blankItem]);
|
||||
const id = nextId.current++;
|
||||
setRows([...rows, { id, value: blankItem }]);
|
||||
// A new location starts empty, so open it and make sure an active filter
|
||||
// doesn't hide the row that was just added.
|
||||
setExpanded([...expanded, id]);
|
||||
setFilter("");
|
||||
scrollToId.current = id;
|
||||
};
|
||||
|
||||
const handleRemove = (idx: number) => {
|
||||
const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx);
|
||||
setValues(newValues);
|
||||
setFormField(newValues);
|
||||
const handleRemove = (id: number) => {
|
||||
const newRows = rows.filter((r: Row) => r.id !== id);
|
||||
setRows(newRows);
|
||||
setExpanded(expanded.filter((i) => i !== id));
|
||||
setAdvVisible(advVisible.filter((i) => i !== id));
|
||||
setFormField(newRows);
|
||||
};
|
||||
|
||||
const handleChange = (idx: number, field: string, fieldValue: string) => {
|
||||
const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
|
||||
setValues(newValues);
|
||||
setFormField(newValues);
|
||||
const handleChange = (id: number, field: string, fieldValue: string) => {
|
||||
const newRows = rows.map((r: Row) => (r.id === id ? { ...r, value: { ...r.value, [field]: fieldValue } } : r));
|
||||
setRows(newRows);
|
||||
setFormField(newRows);
|
||||
};
|
||||
|
||||
const setFormField = (newValues: ProxyLocation[]) => {
|
||||
const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== "");
|
||||
const setFormField = (newRows: Row[]) => {
|
||||
const filtered = newRows.map((r: Row) => r.value).filter((v: ProxyLocation) => v?.path?.trim() !== "");
|
||||
setFieldValue(name, filtered);
|
||||
};
|
||||
|
||||
if (values.length === 0) {
|
||||
const forwardSummary = (item: ProxyLocation) => {
|
||||
if (!item.forwardHost) {
|
||||
return "";
|
||||
}
|
||||
return `${item.forwardScheme}://${item.forwardHost}${item.forwardPort ? `:${item.forwardPort}` : ""}`;
|
||||
};
|
||||
|
||||
// Matches the path as well as the destination, so a location can be found by
|
||||
// the host or port it forwards to and not just by its path.
|
||||
const matchesFilter = (item: ProxyLocation, query: string) =>
|
||||
[item.path, item.forwardScheme, item.forwardHost, item.forwardPort, forwardSummary(item)]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(query);
|
||||
|
||||
const query = filter.trim().toLowerCase();
|
||||
const visibleRows = query ? rows.filter((r: Row) => matchesFilter(r.value, query)) : rows;
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<button type="button" className="btn my-3" onClick={handleAdd}>
|
||||
@@ -61,125 +114,217 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{values.map((item: ProxyLocation, idx: number) => (
|
||||
<div key={idx} className={cn("card", "card-active", "mb-3", styles.locationCard)}>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Location</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="/path"
|
||||
autoComplete="off"
|
||||
value={item.path}
|
||||
onChange={(e) => handleChange(idx, "path", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 text-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn p-0"
|
||||
title="Advanced"
|
||||
onClick={() => toggleAdvVisible(idx)}
|
||||
>
|
||||
<IconSettings size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="forwardScheme">
|
||||
<T id="host.forward-scheme" />
|
||||
</label>
|
||||
<select
|
||||
id="forwardScheme"
|
||||
className="form-control"
|
||||
value={item.forwardScheme}
|
||||
onChange={(e) => handleChange(idx, "forwardScheme", e.target.value)}
|
||||
>
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="forwardHost">
|
||||
<T id="proxy-host.forward-host" />
|
||||
</label>
|
||||
<input
|
||||
id="forwardHost"
|
||||
type="text"
|
||||
className="form-control"
|
||||
required
|
||||
placeholder="eg: 10.0.0.1/path/"
|
||||
value={item.forwardHost}
|
||||
onChange={(e) => handleChange(idx, "forwardHost", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="forwardPort">
|
||||
<T id="host.forward-port" />
|
||||
</label>
|
||||
<input
|
||||
id="forwardPort"
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
className="form-control"
|
||||
required
|
||||
placeholder="eg: 8081"
|
||||
value={item.forwardPort}
|
||||
onChange={(e) => handleChange(idx, "forwardPort", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{advVisible.includes(idx) && (
|
||||
<div className="">
|
||||
<CodeEditor
|
||||
language="nginx"
|
||||
placeholder={intl.formatMessage({ id: "nginx-config.placeholder" })}
|
||||
padding={15}
|
||||
data-color-mode="dark"
|
||||
minHeight={170}
|
||||
indentWidth={2}
|
||||
value={item.advancedConfig}
|
||||
onChange={(e) => handleChange(idx, "advancedConfig", e.target.value)}
|
||||
style={{
|
||||
fontFamily:
|
||||
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
|
||||
borderRadius: "0.3rem",
|
||||
minHeight: "170px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemove(idx);
|
||||
}}
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
{rows.length >= FILTER_THRESHOLD && (
|
||||
<div className={cn("input-group", styles.filter)}>
|
||||
<span className="input-group-text">
|
||||
<IconSearch size={16} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
placeholder={intl.formatMessage({ id: "location.filter" })}
|
||||
aria-label={intl.formatMessage({ id: "location.filter" })}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
{filter ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon"
|
||||
title={intl.formatMessage({ id: "action.clear" })}
|
||||
aria-label={intl.formatMessage({ id: "action.clear" })}
|
||||
onClick={() => setFilter("")}
|
||||
>
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</div>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<button type="button" className="btn btn-sm" onClick={handleAdd}>
|
||||
)}
|
||||
<button type="button" className="btn ms-auto" onClick={handleAdd}>
|
||||
<IconPlus size={16} className="me-1" />
|
||||
<T id="action.add-location" />
|
||||
</button>
|
||||
</div>
|
||||
{visibleRows.length === 0 ? (
|
||||
<div className="text-secondary text-center my-3">
|
||||
<T id="empty-search" />
|
||||
</div>
|
||||
) : (
|
||||
visibleRows.map((row: Row) => {
|
||||
const item = row.value;
|
||||
const isOpen = expanded.includes(row.id);
|
||||
const bodyId = `location-body-${row.id}`;
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
ref={(node) => {
|
||||
if (node && scrollToId.current === row.id) {
|
||||
scrollToId.current = null;
|
||||
node.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}}
|
||||
className={cn("card", "card-active", "mb-2", styles.locationCard)}
|
||||
>
|
||||
<div className={cn("card-header", "p-2", !isOpen && "border-bottom-0")}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggle}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={bodyId}
|
||||
onClick={() => toggleExpanded(row.id)}
|
||||
>
|
||||
{isOpen ? <IconChevronDown size={16} /> : <IconChevronRight size={16} />}
|
||||
<span className={cn("ms-2", styles.path)}>{item.path}</span>
|
||||
<span className={cn("ms-2", "text-secondary", styles.summary)}>
|
||||
{forwardSummary(item)}
|
||||
</span>
|
||||
</button>
|
||||
{item.advancedConfig ? (
|
||||
// Deliberately the same icon as the advanced-config toggle in the
|
||||
// body below, so the marker reads as "this row has that section
|
||||
// filled in" rather than as a decoration of its own.
|
||||
<span
|
||||
className={cn("ms-2", "text-secondary", styles.marker)}
|
||||
role="img"
|
||||
title={intl.formatMessage({ id: "location.advanced-config" })}
|
||||
aria-label={intl.formatMessage({ id: "location.advanced-config" })}
|
||||
>
|
||||
<IconSettings size={16} />
|
||||
</span>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-action ms-2"
|
||||
title={intl.formatMessage({ id: "action.delete" })}
|
||||
aria-label={intl.formatMessage({ id: "action.delete" })}
|
||||
onClick={() => handleRemove(row.id)}
|
||||
>
|
||||
<IconTrash size={16} className="icon" />
|
||||
</button>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="card-body" id={bodyId}>
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Location</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="/path"
|
||||
autoComplete="off"
|
||||
value={item.path}
|
||||
onChange={(e) => handleChange(row.id, "path", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 text-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn p-0"
|
||||
title="Advanced"
|
||||
aria-expanded={advVisible.includes(row.id)}
|
||||
onClick={() => toggleAdvVisible(row.id)}
|
||||
>
|
||||
<IconSettings size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<div className="mb-3">
|
||||
<label
|
||||
className="form-label"
|
||||
htmlFor={`location-forwardScheme-${row.id}`}
|
||||
>
|
||||
<T id="host.forward-scheme" />
|
||||
</label>
|
||||
<select
|
||||
id={`location-forwardScheme-${row.id}`}
|
||||
className="form-control"
|
||||
value={item.forwardScheme}
|
||||
onChange={(e) =>
|
||||
handleChange(row.id, "forwardScheme", e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="mb-3">
|
||||
<label
|
||||
className="form-label"
|
||||
htmlFor={`location-forwardHost-${row.id}`}
|
||||
>
|
||||
<T id="proxy-host.forward-host" />
|
||||
</label>
|
||||
<input
|
||||
id={`location-forwardHost-${row.id}`}
|
||||
type="text"
|
||||
className="form-control"
|
||||
required
|
||||
placeholder="eg: 10.0.0.1/path/"
|
||||
value={item.forwardHost}
|
||||
onChange={(e) =>
|
||||
handleChange(row.id, "forwardHost", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<div className="mb-3">
|
||||
<label
|
||||
className="form-label"
|
||||
htmlFor={`location-forwardPort-${row.id}`}
|
||||
>
|
||||
<T id="host.forward-port" />
|
||||
</label>
|
||||
<input
|
||||
id={`location-forwardPort-${row.id}`}
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
className="form-control"
|
||||
required
|
||||
placeholder="eg: 8081"
|
||||
value={item.forwardPort}
|
||||
onChange={(e) =>
|
||||
handleChange(row.id, "forwardPort", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{advVisible.includes(row.id) && (
|
||||
<div className="">
|
||||
<CodeEditor
|
||||
language="nginx"
|
||||
placeholder={intl.formatMessage({ id: "nginx-config.placeholder" })}
|
||||
padding={15}
|
||||
data-color-mode="dark"
|
||||
minHeight={170}
|
||||
indentWidth={2}
|
||||
value={item.advancedConfig}
|
||||
onChange={(e) => handleChange(row.id, "advancedConfig", e.target.value)}
|
||||
style={{
|
||||
fontFamily:
|
||||
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
|
||||
borderRadius: "0.3rem",
|
||||
minHeight: "170px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ interface Props {
|
||||
requireDomainNames?: boolean; // used for streams
|
||||
color?: string;
|
||||
}
|
||||
export function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceDNSForNew, requireDomainNames, color = "bg-cyan" }: Props) {
|
||||
export function SSLOptionsFields({
|
||||
forHttp = true,
|
||||
forProxyHost = false,
|
||||
forceDNSForNew,
|
||||
requireDomainNames,
|
||||
color = "bg-cyan",
|
||||
}: Props) {
|
||||
const { values, setFieldValue } = useFormikContext();
|
||||
const v: any = values || {};
|
||||
|
||||
@@ -116,10 +122,12 @@ export function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceDN
|
||||
</div>
|
||||
);
|
||||
|
||||
const getHttpAdvancedOptions = () =>(
|
||||
const getHttpAdvancedOptions = () => (
|
||||
<div>
|
||||
<details>
|
||||
<summary className="mb-1"><T id="domains.advanced" /></summary>
|
||||
<summary className="mb-1">
|
||||
<T id="domains.advanced" />
|
||||
</summary>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<Field name="trustForwardedProto">
|
||||
|
||||
@@ -72,7 +72,10 @@ export function SiteHeader() {
|
||||
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<div className="d-md-none">
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: This div is not interactive. */}
|
||||
<div className="p-2 pb-1 pe-1 d-flex align-items-center" onClick={e => e.stopPropagation()}>
|
||||
<div
|
||||
className="p-2 pb-1 pe-1 d-flex align-items-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="ps-2 pe-1 me-auto">
|
||||
<div>{currentUser?.nickname}</div>
|
||||
<div className="mt-1 small text-secondary text-nowrap">
|
||||
|
||||
@@ -143,7 +143,6 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
|
||||
className="nav-link dropdown-toggle"
|
||||
href={item.to}
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="outside"
|
||||
aria-expanded="false"
|
||||
role="button"
|
||||
>
|
||||
@@ -176,13 +175,14 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
|
||||
};
|
||||
|
||||
export function SiteMenu() {
|
||||
const closeMenu = () => setTimeout(() => {
|
||||
const navbarToggler = document.querySelector<HTMLElement>(".navbar-toggler");
|
||||
const navbarMenu = document.querySelector("#navbar-menu");
|
||||
if (navbarToggler && navbarMenu?.classList.contains("show")) {
|
||||
navbarToggler.click();
|
||||
}
|
||||
}, 300);
|
||||
const closeMenu = () =>
|
||||
setTimeout(() => {
|
||||
const navbarToggler = document.querySelector<HTMLElement>(".navbar-toggler");
|
||||
const navbarMenu = document.querySelector("#navbar-menu");
|
||||
if (navbarToggler && navbarMenu?.classList.contains("show")) {
|
||||
navbarToggler.click();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return (
|
||||
<header className="navbar-expand-md">
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Table as ReactTable } from "@tanstack/react-table";
|
||||
import type { Table as ReactTable, RowData } from "@tanstack/react-table";
|
||||
import type { Features } from "./features";
|
||||
|
||||
interface Props {
|
||||
tableInstance: ReactTable<any>;
|
||||
interface Props<TData extends RowData> {
|
||||
tableInstance: ReactTable<Features, TData>;
|
||||
}
|
||||
function EmptyRow({ tableInstance }: Props) {
|
||||
function EmptyRow<TData extends RowData>({ tableInstance }: Props<TData>) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { flexRender } from "@tanstack/react-table";
|
||||
import { flexRender, type RowData } from "@tanstack/react-table";
|
||||
import type { TableLayoutProps } from "src/components";
|
||||
import { EmptyRow } from "./EmptyRow";
|
||||
|
||||
function TableBody<T>(props: TableLayoutProps<T>) {
|
||||
function TableBody<T extends RowData>(props: TableLayoutProps<T>) {
|
||||
const { tableInstance, extraStyles, emptyState } = props;
|
||||
const rows = tableInstance.getRowModel().rows;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IconArrowsSort, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||
import { flexRender } from "@tanstack/react-table";
|
||||
import { flexRender, type RowData } from "@tanstack/react-table";
|
||||
import type { TableLayoutProps } from "src/components";
|
||||
|
||||
function TableHeader<T>(props: TableLayoutProps<T>) {
|
||||
function TableHeader<T extends RowData>(props: TableLayoutProps<T>) {
|
||||
const { tableInstance } = props;
|
||||
const headerGroups = tableInstance.getHeaderGroups();
|
||||
|
||||
|
||||
@@ -61,4 +61,4 @@ const tableEventReducer = (state: any, { type, payload }: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { tableEvents, tableEventReducer };
|
||||
export { tableEventReducer, tableEvents };
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { Table as ReactTable } from "@tanstack/react-table";
|
||||
import type { Table as ReactTable, RowData } from "@tanstack/react-table";
|
||||
import type { Features } from "./features";
|
||||
import { TableBody } from "./TableBody";
|
||||
import { TableHeader } from "./TableHeader";
|
||||
|
||||
interface TableLayoutProps<TFields> {
|
||||
tableInstance: ReactTable<TFields>;
|
||||
interface TableLayoutProps<TFields extends RowData> {
|
||||
tableInstance: ReactTable<Features, TFields>;
|
||||
emptyState?: React.ReactNode;
|
||||
extraStyles?: {
|
||||
row: (rowData: TFields) => any | undefined;
|
||||
};
|
||||
}
|
||||
function TableLayout<TFields>(props: TableLayoutProps<TFields>) {
|
||||
function TableLayout<TFields extends RowData>(props: TableLayoutProps<TFields>) {
|
||||
const hasRows = props.tableInstance.getRowModel().rows.length > 0;
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
columnVisibilityFeature,
|
||||
createSortedRowModel,
|
||||
metaHelper,
|
||||
rowSortingFeature,
|
||||
tableFeatures,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
interface ColumnMeta {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TableMeta {
|
||||
isFetching?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared TanStack Table v9 feature registration for every table in the app.
|
||||
* Sorting and column visibility are used (or their APIs are called
|
||||
* unconditionally, e.g. `getVisibleFlatColumns`/`getVisibleCells`) by the
|
||||
* shared TableLayout/TableHeader/TableBody/EmptyData components, so every
|
||||
* table instance must register them even when a particular table doesn't
|
||||
* wire up controlled sorting state itself.
|
||||
*/
|
||||
const features = tableFeatures({
|
||||
rowSortingFeature,
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
columnVisibilityFeature,
|
||||
columnMeta: metaHelper<ColumnMeta>(),
|
||||
tableMeta: metaHelper<TableMeta>(),
|
||||
});
|
||||
|
||||
type Features = typeof features;
|
||||
|
||||
export { type ColumnMeta, type Features, features, type TableMeta };
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./Formatter";
|
||||
export * from "./features";
|
||||
export * from "./TableHeader";
|
||||
export * from "./TableHelpers";
|
||||
export * from "./TableLayout";
|
||||
|
||||
@@ -21,49 +21,51 @@ import langZh from "./lang/zh.json";
|
||||
import langTr from "./lang/tr.json";
|
||||
import langHu from "./lang/hu.json";
|
||||
import langNo from "./lang/no.json";
|
||||
import langUk from "./lang/uk.json";
|
||||
import langList from "./lang/lang-list.json";
|
||||
|
||||
// first item of each array should be the language code,
|
||||
// not the country code
|
||||
// Remember when adding to this list, also update check-locales.js script
|
||||
const localeOptions = [
|
||||
["en", "en-US", langEn],
|
||||
["de", "de-DE", langDe],
|
||||
["es", "es-ES", langEs],
|
||||
["et", "et-EE", langEt],
|
||||
["pt", "pt-PT", langPt],
|
||||
["fr", "fr-FR", langFr],
|
||||
["ga", "ga-IE", langGa],
|
||||
["ja", "ja-JP", langJa],
|
||||
["it", "it-IT", langIt],
|
||||
["nl", "nl-NL", langNl],
|
||||
["pl", "pl-PL", langPl],
|
||||
["ru", "ru-RU", langRu],
|
||||
["sk", "sk-SK", langSk],
|
||||
["cs", "cs-CZ", langCs],
|
||||
["vi", "vi-VN", langVi],
|
||||
["zh", "zh-CN", langZh],
|
||||
["ko", "ko-KR", langKo],
|
||||
["bg", "bg-BG", langBg],
|
||||
["id", "id-ID", langId],
|
||||
["tr", "tr-TR", langTr],
|
||||
["hu", "hu-HU", langHu],
|
||||
["no", "no-NO", langNo],
|
||||
["en", "en-US", langEn],
|
||||
["de", "de-DE", langDe],
|
||||
["es", "es-ES", langEs],
|
||||
["et", "et-EE", langEt],
|
||||
["pt", "pt-PT", langPt],
|
||||
["fr", "fr-FR", langFr],
|
||||
["ga", "ga-IE", langGa],
|
||||
["ja", "ja-JP", langJa],
|
||||
["it", "it-IT", langIt],
|
||||
["nl", "nl-NL", langNl],
|
||||
["pl", "pl-PL", langPl],
|
||||
["ru", "ru-RU", langRu],
|
||||
["sk", "sk-SK", langSk],
|
||||
["cs", "cs-CZ", langCs],
|
||||
["vi", "vi-VN", langVi],
|
||||
["zh", "zh-CN", langZh],
|
||||
["ko", "ko-KR", langKo],
|
||||
["bg", "bg-BG", langBg],
|
||||
["id", "id-ID", langId],
|
||||
["tr", "tr-TR", langTr],
|
||||
["hu", "hu-HU", langHu],
|
||||
["no", "no-NO", langNo],
|
||||
["uk", "uk-UA", langUk],
|
||||
];
|
||||
|
||||
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
|
||||
const thisLocale = (locale || "en").slice(0, 2);
|
||||
const thisLocale = (locale || "en").slice(0, 2);
|
||||
|
||||
// ensure this lang exists in localeOptions above, otherwise fallback to en
|
||||
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
|
||||
return Object.assign({}, langList, langEn);
|
||||
}
|
||||
// ensure this lang exists in localeOptions above, otherwise fallback to en
|
||||
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
|
||||
return Object.assign({}, langList, langEn);
|
||||
}
|
||||
|
||||
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
|
||||
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
|
||||
};
|
||||
|
||||
const getFlagCodeForLocale = (locale?: string) => {
|
||||
const thisLocale = (locale || "en").slice(0, 2);
|
||||
const thisLocale = (locale || "en").slice(0, 2);
|
||||
|
||||
// only add to this if your flag is different from the locale code
|
||||
const specialCases: Record<string, string> = {
|
||||
@@ -74,27 +76,28 @@ const getFlagCodeForLocale = (locale?: string) => {
|
||||
cs: "cz", // Czechia
|
||||
ga: "ie", // Ireland (Irish)
|
||||
et: "ee", // Estonia
|
||||
uk: "ua", // Ukraine
|
||||
};
|
||||
|
||||
if (specialCases[thisLocale]) {
|
||||
return specialCases[thisLocale].toUpperCase();
|
||||
}
|
||||
return thisLocale.toUpperCase();
|
||||
if (specialCases[thisLocale]) {
|
||||
return specialCases[thisLocale].toUpperCase();
|
||||
}
|
||||
return thisLocale.toUpperCase();
|
||||
};
|
||||
|
||||
const getLocale = (short = false) => {
|
||||
let loc = window.localStorage.getItem("locale");
|
||||
if (!loc) {
|
||||
loc = document.documentElement.lang;
|
||||
}
|
||||
if (short) {
|
||||
return loc.slice(0, 2);
|
||||
}
|
||||
// finally, fallback
|
||||
if (!loc) {
|
||||
loc = "en";
|
||||
}
|
||||
return loc;
|
||||
let loc = window.localStorage.getItem("locale");
|
||||
if (!loc) {
|
||||
loc = document.documentElement.lang;
|
||||
}
|
||||
if (short) {
|
||||
return loc.slice(0, 2);
|
||||
}
|
||||
// finally, fallback
|
||||
if (!loc) {
|
||||
loc = "en";
|
||||
}
|
||||
return loc;
|
||||
};
|
||||
|
||||
const cache = createIntlCache();
|
||||
@@ -103,41 +106,41 @@ const initialMessages = loadMessages(getLocale());
|
||||
let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);
|
||||
|
||||
const changeLocale = (locale: string): void => {
|
||||
const messages = loadMessages(locale);
|
||||
intl = createIntl({ locale, messages }, cache);
|
||||
window.localStorage.setItem("locale", locale);
|
||||
document.documentElement.lang = locale;
|
||||
const messages = loadMessages(locale);
|
||||
intl = createIntl({ locale, messages }, cache);
|
||||
window.localStorage.setItem("locale", locale);
|
||||
document.documentElement.lang = locale;
|
||||
};
|
||||
|
||||
// This is a translation component that wraps the translation in a span with a data
|
||||
// attribute so devs can inspect the element to see the translation ID
|
||||
const T = ({
|
||||
id,
|
||||
data,
|
||||
tData,
|
||||
id,
|
||||
data,
|
||||
tData,
|
||||
}: {
|
||||
id: string;
|
||||
data?: Record<string, string | number | undefined>;
|
||||
tData?: Record<string, string>;
|
||||
id: string;
|
||||
data?: Record<string, string | number | undefined>;
|
||||
tData?: Record<string, string>;
|
||||
}) => {
|
||||
const translatedData: Record<string, string> = {};
|
||||
if (tData) {
|
||||
// iterate over tData and translate each value
|
||||
Object.entries(tData).forEach(([key, value]) => {
|
||||
translatedData[key] = intl.formatMessage({ id: value });
|
||||
});
|
||||
}
|
||||
return (
|
||||
<span data-translation-id={id}>
|
||||
{intl.formatMessage(
|
||||
{ id },
|
||||
{
|
||||
...data,
|
||||
...translatedData,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
const translatedData: Record<string, string> = {};
|
||||
if (tData) {
|
||||
// iterate over tData and translate each value
|
||||
Object.entries(tData).forEach(([key, value]) => {
|
||||
translatedData[key] = intl.formatMessage({ id: value });
|
||||
});
|
||||
}
|
||||
return (
|
||||
<span data-translation-id={id}>
|
||||
{intl.formatMessage(
|
||||
{ id },
|
||||
{
|
||||
...data,
|
||||
...translatedData,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
//console.log("L:", localeOptions);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
fromUnixTime,
|
||||
type IntlFormatFormatOptions,
|
||||
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;
|
||||
|
||||
@@ -3,4 +3,4 @@ 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";
|
||||
export * as Streams from "./Streams.md";
|
||||
|
||||
@@ -19,21 +19,21 @@ import * as vi from "./vi/index";
|
||||
import * as zh from "./zh/index";
|
||||
import * as tr from "./tr/index";
|
||||
import * as hu from "./hu/index";
|
||||
import * as uk from "./uk/index";
|
||||
|
||||
const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu };
|
||||
|
||||
const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu, uk };
|
||||
|
||||
const fallbackLang = "en";
|
||||
|
||||
export const getHelpFile = (lang: string, section: string): string => {
|
||||
if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
|
||||
return items[lang][section].default;
|
||||
}
|
||||
// Fallback to English
|
||||
if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
|
||||
return items[fallbackLang][section].default;
|
||||
}
|
||||
throw new Error(`Cannot load help doc for ${lang}-${section}`);
|
||||
if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
|
||||
return items[lang][section].default;
|
||||
}
|
||||
// Fallback to English
|
||||
if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
|
||||
return items[fallbackLang][section].default;
|
||||
}
|
||||
throw new Error(`Cannot load help doc for ${lang}-${section}`);
|
||||
};
|
||||
|
||||
export default items;
|
||||
|
||||
@@ -4,4 +4,3 @@ export * as DeadHosts from "./DeadHosts.md";
|
||||
export * as ProxyHosts from "./ProxyHosts.md";
|
||||
export * as RedirectionHosts from "./RedirectionHosts.md";
|
||||
export * as Streams from "./Streams.md";
|
||||
|
||||
|
||||
@@ -4,4 +4,3 @@ export * as DeadHosts from "./DeadHosts.md";
|
||||
export * as ProxyHosts from "./ProxyHosts.md";
|
||||
export * as RedirectionHosts from "./RedirectionHosts.md";
|
||||
export * as Streams from "./Streams.md";
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
## Що таке список доступу?
|
||||
|
||||
Списки доступу дають змогу створити список дозволених або заборонених IP-адрес клієнтів, а також налаштувати автентифікацію для проксі-хостів за допомогою базової HTTP-автентифікації.
|
||||
|
||||
Для одного списку доступу можна налаштувати кілька правил для клієнтів, імен користувачів і паролів, а потім застосувати його до одного або кількох _проксі-хостів_.
|
||||
|
||||
Це особливо корисно для проксійованих вебсервісів, які не мають власних механізмів автентифікації, або коли потрібно захистити їх від невідомих клієнтів.
|
||||
@@ -0,0 +1,21 @@
|
||||
## Довідка щодо сертифікатів
|
||||
|
||||
### HTTP-сертифікат
|
||||
|
||||
Під час перевірки сертифіката через HTTP сервери Let's Encrypt спробують отримати доступ до ваших доменів через HTTP (не HTTPS!). Якщо перевірка буде успішною, вони видадуть сертифікат.
|
||||
|
||||
Для цього методу потрібно створити _проксі-хост_ для відповідних доменів. Він має бути доступним через HTTP і вказувати на цей екземпляр Nginx. Після отримання сертифіката можна налаштувати _проксі-хост_ на використання цього сертифіката для HTTPS-з'єднань. Однак для автоматичного поновлення сертифіката _проксі-хост_ і надалі має бути доступним через HTTP.
|
||||
|
||||
Цей спосіб _не підтримує_ підстановчі домени.
|
||||
|
||||
### DNS-сертифікат
|
||||
|
||||
Для перевірки сертифіката через DNS потрібно використати плагін DNS-провайдера. За допомогою цього плагіна буде створено тимчасові записи у вашому домені. Let's Encrypt перевірить ці записи, щоб підтвердити, що домен належить вам, і в разі успішної перевірки видасть сертифікат.
|
||||
|
||||
Перед запитом такого сертифіката не потрібно створювати _проксі-хост_ або налаштовувати для нього доступ через HTTP.
|
||||
|
||||
Цей спосіб _підтримує_ підстановчі домени.
|
||||
|
||||
### Власний сертифікат
|
||||
|
||||
Скористайтеся цим варіантом, щоб завантажити власний SSL-сертифікат, виданий вашим центром сертифікації.
|
||||
@@ -0,0 +1,7 @@
|
||||
## Що таке 404-хост?
|
||||
|
||||
404-хост — це хост, налаштований для показу сторінки з помилкою 404.
|
||||
|
||||
Він може бути корисним, якщо ваш домен проіндексовано пошуковими системами й ви хочете показати зрозумілішу сторінку помилки або повідомити пошуковим роботам, що сторінки домену більше не існують.
|
||||
|
||||
Також цей хост дає змогу відстежувати звернення до нього в журналах і переглядати джерела переходів.
|
||||
@@ -0,0 +1,7 @@
|
||||
## Що таке проксі-хост?
|
||||
|
||||
Проксі-хост — це вхідна точка для вебсервісу, до якого потрібно перенаправляти запити.
|
||||
|
||||
Він дає змогу за потреби завершувати SSL-з'єднання для сервісу, який може не мати вбудованої підтримки SSL.
|
||||
|
||||
Проксі-хости — найпоширеніший спосіб використання Nginx Proxy Manager.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Що таке редирект-хост?
|
||||
|
||||
Редирект-хост перенаправляє запити з вхідного домену на інший домен.
|
||||
|
||||
Найчастіше такий хост використовують після зміни домену вебсайту, коли в пошукових системах або на інших сайтах ще залишилися посилання на старий домен.
|
||||
@@ -0,0 +1,5 @@
|
||||
## Що таке потік?
|
||||
|
||||
Потік — це відносно нова функція Nginx, яка дає змогу перенаправляти TCP/UDP-трафік безпосередньо на інший комп'ютер у мережі.
|
||||
|
||||
Це може бути корисним, якщо ви використовуєте ігрові, FTP- або SSH-сервери.
|
||||
@@ -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,4 +1,61 @@
|
||||
{
|
||||
"2fa.backup-codes-remaining": {
|
||||
"defaultMessage": "Verbleibende Backup-Codes: {count}"
|
||||
},
|
||||
"2fa.backup-warning": {
|
||||
"defaultMessage": "Bewahre diese Backup-Codes an einem sicheren Ort auf. Jeder Code kann nur einmal verwendet werden."
|
||||
},
|
||||
"2fa.disable": {
|
||||
"defaultMessage": "Zwei-Faktor-Authentifizierung deaktivieren"
|
||||
},
|
||||
"2fa.disable-confirm": {
|
||||
"defaultMessage": "2FA deaktivieren"
|
||||
},
|
||||
"2fa.disable-warning": {
|
||||
"defaultMessage": "Das Deaktivieren der Zwei-Faktor-Authentifizierung macht dein Konto weniger sicher."
|
||||
},
|
||||
"2fa.disabled": {
|
||||
"defaultMessage": "Deaktiviert"
|
||||
},
|
||||
"2fa.done": {
|
||||
"defaultMessage": "Ich habe meine Backup-Codes gespeichert"
|
||||
},
|
||||
"2fa.enable": {
|
||||
"defaultMessage": "Zwei-Faktor-Authentifizierung aktivieren"
|
||||
},
|
||||
"2fa.enabled": {
|
||||
"defaultMessage": "Aktiviert"
|
||||
},
|
||||
"2fa.enter-code": {
|
||||
"defaultMessage": "Verifizierungscode eingeben"
|
||||
},
|
||||
"2fa.enter-code-disable": {
|
||||
"defaultMessage": "Verifizierungscode zum Deaktivieren eingeben"
|
||||
},
|
||||
"2fa.regenerate": {
|
||||
"defaultMessage": "Neu generieren"
|
||||
},
|
||||
"2fa.regenerate-backup": {
|
||||
"defaultMessage": "Backup-Codes neu generieren"
|
||||
},
|
||||
"2fa.regenerate-instructions": {
|
||||
"defaultMessage": "Gib einen Verifizierungscode ein, um neue Backup-Codes zu generieren. Deine alten Codes werden dadurch ungültig."
|
||||
},
|
||||
"2fa.secret-key": {
|
||||
"defaultMessage": "Geheimer Schlüssel"
|
||||
},
|
||||
"2fa.setup-instructions": {
|
||||
"defaultMessage": "Scanne diesen QR-Code mit deiner Authenticator-App oder gib den geheimen Schlüssel manuell ein."
|
||||
},
|
||||
"2fa.status": {
|
||||
"defaultMessage": "Status"
|
||||
},
|
||||
"2fa.title": {
|
||||
"defaultMessage": "Zwei-Faktor-Authentifizierung"
|
||||
},
|
||||
"2fa.verify-enable": {
|
||||
"defaultMessage": "Verifizieren und aktivieren"
|
||||
},
|
||||
"access-list": {
|
||||
"defaultMessage": "Zugriffsliste"
|
||||
},
|
||||
@@ -23,6 +80,9 @@
|
||||
"access-list.public.subtitle": {
|
||||
"defaultMessage": "Keine Authentifizierung erforderlich"
|
||||
},
|
||||
"access-list.rule-source.placeholder": {
|
||||
"defaultMessage": "192.168.1.100 oder 192.168.1.0/24 oder 2001:0db8::/32"
|
||||
},
|
||||
"access-list.satisfy-any": {
|
||||
"defaultMessage": "Satisfy Any"
|
||||
},
|
||||
@@ -38,12 +98,18 @@
|
||||
"action.add-location": {
|
||||
"defaultMessage": "Pfad hinzufügen"
|
||||
},
|
||||
"action.allow": {
|
||||
"defaultMessage": "Erlauben"
|
||||
},
|
||||
"action.close": {
|
||||
"defaultMessage": "Schließen"
|
||||
},
|
||||
"action.delete": {
|
||||
"defaultMessage": "Löschen"
|
||||
},
|
||||
"action.deny": {
|
||||
"defaultMessage": "Verweigern"
|
||||
},
|
||||
"action.disable": {
|
||||
"defaultMessage": "Deaktivieren"
|
||||
},
|
||||
@@ -68,6 +134,9 @@
|
||||
"auditlogs": {
|
||||
"defaultMessage": "Protokolle"
|
||||
},
|
||||
"auto": {
|
||||
"defaultMessage": "Automatisch"
|
||||
},
|
||||
"cancel": {
|
||||
"defaultMessage": "Abbrechen"
|
||||
},
|
||||
@@ -128,6 +197,9 @@
|
||||
"certificates.dns.provider": {
|
||||
"defaultMessage": "DNS Provider"
|
||||
},
|
||||
"certificates.dns.provider.placeholder": {
|
||||
"defaultMessage": "Anbieter auswählen..."
|
||||
},
|
||||
"certificates.dns.warning": {
|
||||
"defaultMessage": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation."
|
||||
},
|
||||
@@ -275,6 +347,9 @@
|
||||
"domain-names.wildcards-not-supported": {
|
||||
"defaultMessage": "Wildcards werden für diese Zertifizierungsstelle nicht unterstützt."
|
||||
},
|
||||
"domains.advanced": {
|
||||
"defaultMessage": "Erweitert"
|
||||
},
|
||||
"domains.force-ssl": {
|
||||
"defaultMessage": "Erzwinge SSL"
|
||||
},
|
||||
@@ -287,6 +362,9 @@
|
||||
"domains.http2-support": {
|
||||
"defaultMessage": "HTTP/2 Support"
|
||||
},
|
||||
"domains.trust-forwarded-proto": {
|
||||
"defaultMessage": "Upstream Forwarded-Proto-Header vertrauen"
|
||||
},
|
||||
"domains.use-dns": {
|
||||
"defaultMessage": "Nutze DNS Challenge"
|
||||
},
|
||||
@@ -354,7 +432,7 @@
|
||||
"defaultMessage": "Pfad beibehalten"
|
||||
},
|
||||
"host.flags.protocols": {
|
||||
"defaultMessage": "Protokole"
|
||||
"defaultMessage": "Protokolle"
|
||||
},
|
||||
"host.flags.websockets-upgrade": {
|
||||
"defaultMessage": "Websockets Support"
|
||||
@@ -383,6 +461,21 @@
|
||||
"loading": {
|
||||
"defaultMessage": "Laden…"
|
||||
},
|
||||
"login.2fa-code": {
|
||||
"defaultMessage": "Verifizierungscode"
|
||||
},
|
||||
"login.2fa-code-placeholder": {
|
||||
"defaultMessage": "Code eingeben"
|
||||
},
|
||||
"login.2fa-description": {
|
||||
"defaultMessage": "Gib den Code aus deiner Authenticator-App ein"
|
||||
},
|
||||
"login.2fa-title": {
|
||||
"defaultMessage": "Zwei-Faktor-Authentifizierung"
|
||||
},
|
||||
"login.2fa-verify": {
|
||||
"defaultMessage": "Verifizieren"
|
||||
},
|
||||
"login.title": {
|
||||
"defaultMessage": "Anmelden"
|
||||
},
|
||||
@@ -530,6 +623,24 @@
|
||||
"redirection-hosts.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
|
||||
},
|
||||
"redirection-hosts.http-code.300": {
|
||||
"defaultMessage": "300 Mehrere Auswahlmöglichkeiten"
|
||||
},
|
||||
"redirection-hosts.http-code.301": {
|
||||
"defaultMessage": "301 Dauerhaft verschoben"
|
||||
},
|
||||
"redirection-hosts.http-code.302": {
|
||||
"defaultMessage": "302 Vorübergehend verschoben"
|
||||
},
|
||||
"redirection-hosts.http-code.303": {
|
||||
"defaultMessage": "303 Siehe andere"
|
||||
},
|
||||
"redirection-hosts.http-code.307": {
|
||||
"defaultMessage": "307 Temporäre Umleitung"
|
||||
},
|
||||
"redirection-hosts.http-code.308": {
|
||||
"defaultMessage": "308 Permanente Umleitung"
|
||||
},
|
||||
"role.admin": {
|
||||
"defaultMessage": "Administrator"
|
||||
},
|
||||
@@ -587,6 +698,9 @@
|
||||
"stream.forward-host": {
|
||||
"defaultMessage": "Forward Host"
|
||||
},
|
||||
"stream.forward-host.placeholder": {
|
||||
"defaultMessage": "example.com oder 10.0.0.1 oder 2001:db8:3333:4444:5555:6666:7777:8888"
|
||||
},
|
||||
"stream.incoming-port": {
|
||||
"defaultMessage": "Incoming Port"
|
||||
},
|
||||
@@ -605,6 +719,9 @@
|
||||
"test": {
|
||||
"defaultMessage": "Test"
|
||||
},
|
||||
"update-available": {
|
||||
"defaultMessage": "Update verfügbar: {latestVersion}"
|
||||
},
|
||||
"user": {
|
||||
"defaultMessage": "User"
|
||||
},
|
||||
@@ -647,10 +764,13 @@
|
||||
"user.switch-light": {
|
||||
"defaultMessage": "Zum Light Mode wechseln"
|
||||
},
|
||||
"user.two-factor": {
|
||||
"defaultMessage": "Zwei-Faktor-Auth"
|
||||
},
|
||||
"username": {
|
||||
"defaultMessage": "Benutzername"
|
||||
},
|
||||
"users": {
|
||||
"defaultMessage": "Benutzer"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,9 @@
|
||||
"action.allow": {
|
||||
"defaultMessage": "Allow"
|
||||
},
|
||||
"action.clear": {
|
||||
"defaultMessage": "Clear"
|
||||
},
|
||||
"action.close": {
|
||||
"defaultMessage": "Close"
|
||||
},
|
||||
@@ -461,6 +464,12 @@
|
||||
"loading": {
|
||||
"defaultMessage": "Loading…"
|
||||
},
|
||||
"location.advanced-config": {
|
||||
"defaultMessage": "Has custom Nginx configuration"
|
||||
},
|
||||
"location.filter": {
|
||||
"defaultMessage": "Filter by path or destination"
|
||||
},
|
||||
"login.2fa-code": {
|
||||
"defaultMessage": "Verification Code"
|
||||
},
|
||||
|
||||
@@ -64,5 +64,8 @@
|
||||
},
|
||||
"locale-no-NO": {
|
||||
"defaultMessage": "Norsk"
|
||||
},
|
||||
"locale-uk-UA": {
|
||||
"defaultMessage": "Українська"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
"action.allow": {
|
||||
"defaultMessage": "İzin Ver"
|
||||
},
|
||||
"action.clear": {
|
||||
"defaultMessage": "Temizle"
|
||||
},
|
||||
"action.close": {
|
||||
"defaultMessage": "Kapat"
|
||||
},
|
||||
@@ -386,6 +389,12 @@
|
||||
"loading": {
|
||||
"defaultMessage": "Yükleniyor…"
|
||||
},
|
||||
"location.advanced-config": {
|
||||
"defaultMessage": "Özel Nginx yapılandırması var"
|
||||
},
|
||||
"location.filter": {
|
||||
"defaultMessage": "Yol veya hedefe göre filtrele"
|
||||
},
|
||||
"login.title": {
|
||||
"defaultMessage": "Hesabınıza giriş yapın"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,785 @@
|
||||
{
|
||||
"2fa.backup-codes-remaining": {
|
||||
"defaultMessage": "Резервних кодів залишилося: {count}"
|
||||
},
|
||||
"2fa.backup-warning": {
|
||||
"defaultMessage": "Збережіть ці резервні коди в безпечному місці. Кожен код можна використати лише один раз."
|
||||
},
|
||||
"2fa.disable": {
|
||||
"defaultMessage": "Вимкнути двофакторну автентифікацію"
|
||||
},
|
||||
"2fa.disable-confirm": {
|
||||
"defaultMessage": "Вимкнути 2FA"
|
||||
},
|
||||
"2fa.disable-warning": {
|
||||
"defaultMessage": "Вимкнення двофакторної автентифікації знизить рівень безпеки вашого облікового запису."
|
||||
},
|
||||
"2fa.disabled": {
|
||||
"defaultMessage": "Вимкнено"
|
||||
},
|
||||
"2fa.done": {
|
||||
"defaultMessage": "Резервні коди збережено"
|
||||
},
|
||||
"2fa.enable": {
|
||||
"defaultMessage": "Увімкнути двофакторну автентифікацію"
|
||||
},
|
||||
"2fa.enabled": {
|
||||
"defaultMessage": "Увімкнено"
|
||||
},
|
||||
"2fa.enter-code": {
|
||||
"defaultMessage": "Введіть код підтвердження"
|
||||
},
|
||||
"2fa.enter-code-disable": {
|
||||
"defaultMessage": "Введіть код підтвердження для вимкнення"
|
||||
},
|
||||
"2fa.regenerate": {
|
||||
"defaultMessage": "Згенерувати повторно"
|
||||
},
|
||||
"2fa.regenerate-backup": {
|
||||
"defaultMessage": "Повторно згенерувати резервні коди"
|
||||
},
|
||||
"2fa.regenerate-instructions": {
|
||||
"defaultMessage": "Введіть код підтвердження, щоб згенерувати нові резервні коди. Ваші старі коди стануть недійсними."
|
||||
},
|
||||
"2fa.secret-key": {
|
||||
"defaultMessage": "Секретний ключ"
|
||||
},
|
||||
"2fa.setup-instructions": {
|
||||
"defaultMessage": "Відскануйте цей QR-код у застосунку-автентифікаторі або введіть секретний ключ вручну."
|
||||
},
|
||||
"2fa.status": {
|
||||
"defaultMessage": "Статус"
|
||||
},
|
||||
"2fa.title": {
|
||||
"defaultMessage": "Двофакторна автентифікація"
|
||||
},
|
||||
"2fa.verify-enable": {
|
||||
"defaultMessage": "Підтвердити й увімкнути"
|
||||
},
|
||||
"access-list": {
|
||||
"defaultMessage": "Список доступу"
|
||||
},
|
||||
"access-list.access-count": {
|
||||
"defaultMessage": "{count} {count, plural, one {правило} few {правила} many {правил} other {правила}}"
|
||||
},
|
||||
"access-list.auth-count": {
|
||||
"defaultMessage": "{count} {count, plural, one {користувач} few {користувача} many {користувачів} other {користувача}}"
|
||||
},
|
||||
"access-list.help-rules-last": {
|
||||
"defaultMessage": "Якщо є хоча б одне правило, правило 'заборонити все' буде додано останнім"
|
||||
},
|
||||
"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 {користувач} few {користувача} many {користувачів} other {користувача}}, {rules} {rules, plural, one {правило} few {правила} many {правил} other {правила}} - створено: {date}"
|
||||
},
|
||||
"access-lists": {
|
||||
"defaultMessage": "Списки доступу"
|
||||
},
|
||||
"action.add": {
|
||||
"defaultMessage": "Додати"
|
||||
},
|
||||
"action.add-location": {
|
||||
"defaultMessage": "Додати маршрут"
|
||||
},
|
||||
"action.allow": {
|
||||
"defaultMessage": "Дозволити"
|
||||
},
|
||||
"action.clear": {
|
||||
"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? Переконайтеся, що ваш домен вказує на IP-адресу, де запущено ваш екземпляр NPM."
|
||||
},
|
||||
"certificates.http.reachability-wrong-data": {
|
||||
"defaultMessage": "На цьому домені знайдено сервер, але він повернув неочікувані дані. Це сервер NPM? Переконайтеся, що ваш домен вказує на IP-адресу, де запущено ваш екземпляр 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-хост} few {404-хоста} many {404-хостів} other {404-хоста}}"
|
||||
},
|
||||
"disabled": {
|
||||
"defaultMessage": "Вимкнено"
|
||||
},
|
||||
"domain-names": {
|
||||
"defaultMessage": "Домени"
|
||||
},
|
||||
"domain-names.max": {
|
||||
"defaultMessage": "Максимум {count} доменів"
|
||||
},
|
||||
"domain-names.placeholder": {
|
||||
"defaultMessage": "Почніть вводити, щоб додати домен..."
|
||||
},
|
||||
"domain-names.wildcards-not-permitted": {
|
||||
"defaultMessage": "Підстановчі домени не дозволені для цього типу"
|
||||
},
|
||||
"domain-names.wildcards-not-supported": {
|
||||
"defaultMessage": "Підстановчі домени не підтримуються цим центром сертифікації"
|
||||
},
|
||||
"domains.advanced": {
|
||||
"defaultMessage": "Розширені налаштування"
|
||||
},
|
||||
"domains.force-ssl": {
|
||||
"defaultMessage": "Завжди SSL"
|
||||
},
|
||||
"domains.hsts-enabled": {
|
||||
"defaultMessage": "Підтримка HSTS"
|
||||
},
|
||||
"domains.hsts-subdomains": {
|
||||
"defaultMessage": "Піддомени HSTS"
|
||||
},
|
||||
"domains.http2-support": {
|
||||
"defaultMessage": "Підтримка HTTP/2"
|
||||
},
|
||||
"domains.trust-forwarded-proto": {
|
||||
"defaultMessage": "Довіряти заголовкам X-Forwarded-Proto від upstream-сервера"
|
||||
},
|
||||
"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} {max, plural, one {символ} few {символи} many {символів} other {символа}}"
|
||||
},
|
||||
"error.max-domains": {
|
||||
"defaultMessage": "Занадто багато доменів, максимум {max}"
|
||||
},
|
||||
"error.maximum": {
|
||||
"defaultMessage": "Максимум {max}"
|
||||
},
|
||||
"error.min-character-length": {
|
||||
"defaultMessage": "Мінімальна довжина {min} {min, plural, one {символ} few {символи} many {символів} other {символа}}"
|
||||
},
|
||||
"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": "Підтримка WebSocket"
|
||||
},
|
||||
"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": "Завантаження…"
|
||||
},
|
||||
"location.advanced-config": {
|
||||
"defaultMessage": "Має власну конфігурацію Nginx"
|
||||
},
|
||||
"location.filter": {
|
||||
"defaultMessage": "Фільтрувати за шляхом або призначенням"
|
||||
},
|
||||
"login.2fa-code": {
|
||||
"defaultMessage": "Код підтвердження"
|
||||
},
|
||||
"login.2fa-code-placeholder": {
|
||||
"defaultMessage": "Введіть код"
|
||||
},
|
||||
"login.2fa-description": {
|
||||
"defaultMessage": "Введіть код із застосунку-автентифікатора"
|
||||
},
|
||||
"login.2fa-title": {
|
||||
"defaultMessage": "Двофакторна автентифікація"
|
||||
},
|
||||
"login.2fa-verify": {
|
||||
"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 {проксі-хост} few {проксі-хоста} many {проксі-хостів} 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 {редирект-хост} few {редирект-хоста} many {редирект-хостів} other {редирект-хоста}}"
|
||||
},
|
||||
"redirection-hosts.http-code.300": {
|
||||
"defaultMessage": "300 Кілька варіантів"
|
||||
},
|
||||
"redirection-hosts.http-code.301": {
|
||||
"defaultMessage": "301 Переміщено назавжди"
|
||||
},
|
||||
"redirection-hosts.http-code.302": {
|
||||
"defaultMessage": "302 Тимчасово переміщено"
|
||||
},
|
||||
"redirection-hosts.http-code.303": {
|
||||
"defaultMessage": "303 Переглянути інше"
|
||||
},
|
||||
"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": "Що показувати, коли Nginx отримує невідомий хост"
|
||||
},
|
||||
"settings.default-site.html": {
|
||||
"defaultMessage": "Власний HTML"
|
||||
},
|
||||
"settings.default-site.html.placeholder": {
|
||||
"defaultMessage": ""
|
||||
},
|
||||
"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 {потік} few {потоки} many {потоків} 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": "Увімкнути світлу тему"
|
||||
},
|
||||
"user.two-factor": {
|
||||
"defaultMessage": "Двофакторна автентифікація"
|
||||
},
|
||||
"username": {
|
||||
"defaultMessage": "Ім'я користувача"
|
||||
},
|
||||
"users": {
|
||||
"defaultMessage": "Користувачі"
|
||||
}
|
||||
}
|
||||
@@ -70,11 +70,7 @@ const DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPro
|
||||
<label htmlFor="keyType" className="form-label">
|
||||
<T id="certificates.key-type" />
|
||||
</label>
|
||||
<select
|
||||
id="keyType"
|
||||
className="form-select"
|
||||
{...field}
|
||||
>
|
||||
<select id="keyType" className="form-select" {...field}>
|
||||
<option value="rsa">
|
||||
<T id="certificates.key-type-rsa" />
|
||||
</option>
|
||||
|
||||
@@ -151,11 +151,7 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPr
|
||||
<label htmlFor="keyType" className="form-label">
|
||||
<T id="certificates.key-type" />
|
||||
</label>
|
||||
<select
|
||||
id="keyType"
|
||||
className="form-select"
|
||||
{...field}
|
||||
>
|
||||
<select id="keyType" className="form-select" {...field}>
|
||||
<option value="rsa">
|
||||
<T id="certificates.key-type-rsa" />
|
||||
</option>
|
||||
|
||||
@@ -162,7 +162,9 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
|
||||
required
|
||||
{...field}
|
||||
>
|
||||
<option value="auto"><T id="auto" /></option>
|
||||
<option value="auto">
|
||||
<T id="auto" />
|
||||
</option>
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
</select>
|
||||
@@ -224,12 +226,24 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
|
||||
required
|
||||
{...field}
|
||||
>
|
||||
<option value="300"><T id="redirection-hosts.http-code.300" /></option>
|
||||
<option value="301"><T id="redirection-hosts.http-code.301" /></option>
|
||||
<option value="302"><T id="redirection-hosts.http-code.302" /></option>
|
||||
<option value="303"><T id="redirection-hosts.http-code.303" /></option>
|
||||
<option value="307"><T id="redirection-hosts.http-code.307" /></option>
|
||||
<option value="308"><T id="redirection-hosts.http-code.308" /></option>
|
||||
<option value="300">
|
||||
<T id="redirection-hosts.http-code.300" />
|
||||
</option>
|
||||
<option value="301">
|
||||
<T id="redirection-hosts.http-code.301" />
|
||||
</option>
|
||||
<option value="302">
|
||||
<T id="redirection-hosts.http-code.302" />
|
||||
</option>
|
||||
<option value="303">
|
||||
<T id="redirection-hosts.http-code.303" />
|
||||
</option>
|
||||
<option value="307">
|
||||
<T id="redirection-hosts.http-code.307" />
|
||||
</option>
|
||||
<option value="308">
|
||||
<T id="redirection-hosts.http-code.308" />
|
||||
</option>
|
||||
</select>
|
||||
{form.errors.forwardHttpCode ? (
|
||||
<div className="invalid-feedback">
|
||||
|
||||
@@ -154,7 +154,9 @@ const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
type="text"
|
||||
className={`form-control ${form.errors.forwardingHost && form.touched.forwardingHost ? "is-invalid" : ""}`}
|
||||
required
|
||||
placeholder={intl.formatMessage({ id: "stream.forward-host.placeholder" })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: "stream.forward-host.placeholder",
|
||||
})}
|
||||
{...field}
|
||||
/>
|
||||
{form.errors.forwardingHost ? (
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.qrcode {
|
||||
max-width: "200px";
|
||||
height: "auto";
|
||||
border: 6px solid #fff;
|
||||
}
|
||||
@@ -3,16 +3,12 @@ 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 QRCode from "react-qr-code";
|
||||
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";
|
||||
import styles from "./TwoFactorModal.module.css";
|
||||
|
||||
type Step = "loading" | "status" | "setup" | "verify" | "backup" | "disable";
|
||||
|
||||
@@ -137,12 +133,7 @@ const TwoFactorModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
{!isEnabled ? (
|
||||
<Button
|
||||
fullWidth
|
||||
color="azure"
|
||||
onClick={handleStartSetup}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
<Button fullWidth color="azure" onClick={handleStartSetup} isLoading={isSubmitting}>
|
||||
<T id="2fa.enable" />
|
||||
</Button>
|
||||
) : (
|
||||
@@ -166,12 +157,7 @@ const TwoFactorModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
<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" }}
|
||||
/>
|
||||
<QRCode value={setupData.otpauthUrl} size={200} className={styles.qrcode} />
|
||||
</div>
|
||||
<label className="mb-3 d-block">
|
||||
<span className="form-label small text-muted">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { createColumnHelper, useTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { AccessList } from "src/api/backend";
|
||||
import { EmptyData, GravatarFormatter, HasPermission, ValueWithDateFormatter } from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions";
|
||||
@@ -16,7 +17,7 @@ interface Props {
|
||||
onNew?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onNew }: Props) {
|
||||
const columnHelper = createColumnHelper<AccessList>();
|
||||
const columnHelper = createColumnHelper<Features, AccessList>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: any) => row.owner, {
|
||||
@@ -114,11 +115,10 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
[columnHelper, onEdit, onDelete],
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<AccessList>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { createColumnHelper, useTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { AuditLog } from "src/api/backend";
|
||||
import { EventFormatter, GravatarFormatter } from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
|
||||
@@ -11,10 +12,10 @@ interface Props {
|
||||
onSelectItem?: (id: number) => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, onSelectItem }: Props) {
|
||||
const columnHelper = createColumnHelper<AuditLog>();
|
||||
const columnHelper = createColumnHelper<Features, AuditLog>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: AuditLog) => row.user, {
|
||||
columnHelper.accessor((row: any) => row.user, {
|
||||
id: "user.avatar",
|
||||
cell: (info: any) => {
|
||||
const value = info.getValue();
|
||||
@@ -24,7 +25,7 @@ export default function Table({ data, isFetching, onSelectItem }: Props) {
|
||||
className: "w-1",
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor((row: AuditLog) => row, {
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "objectType",
|
||||
header: intl.formatMessage({ id: "column.event" }),
|
||||
cell: (info: any) => {
|
||||
@@ -55,11 +56,10 @@ export default function Table({ data, isFetching, onSelectItem }: Props) {
|
||||
[columnHelper, onSelectItem],
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<AuditLog>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconDotsVertical, IconDownload, IconRefresh, IconTrash } from "@tabler/icons-react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { createColumnHelper, useTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { Certificate } from "src/api/backend";
|
||||
import {
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
GravatarFormatter,
|
||||
HasPermission,
|
||||
} from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
|
||||
@@ -24,7 +25,7 @@ interface Props {
|
||||
onDownload?: (id: number) => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, onDelete, onRenew, onDownload, isFiltered }: Props) {
|
||||
const columnHelper = createColumnHelper<Certificate>();
|
||||
const columnHelper = createColumnHelper<Features, Certificate>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: any) => row.owner, {
|
||||
@@ -164,11 +165,10 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
|
||||
[columnHelper, onDelete, onRenew, onDownload],
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<Certificate>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
@@ -193,9 +193,7 @@ export default function Login() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-md">
|
||||
<div className="card-body">
|
||||
{twoFactorChallenge ? <TwoFactorForm /> : <LoginForm />}
|
||||
</div>
|
||||
<div className="card-body">{twoFactorChallenge ? <TwoFactorForm /> : <LoginForm />}</div>
|
||||
</div>
|
||||
<div className="text-center text-secondary mt-3">{getVersion()}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
|
||||
import {
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { createColumnHelper, type SortingState, useTable } from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { DeadHost } from "src/api/backend";
|
||||
import {
|
||||
@@ -16,6 +10,7 @@ import {
|
||||
HasPermission,
|
||||
TrueFalseFormatter,
|
||||
} from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions";
|
||||
@@ -30,7 +25,7 @@ interface Props {
|
||||
onNew?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
|
||||
const columnHelper = createColumnHelper<DeadHost>();
|
||||
const columnHelper = createColumnHelper<Features, DeadHost>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: any) => row.owner, {
|
||||
@@ -47,7 +42,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "domainNames",
|
||||
header: intl.formatMessage({ id: "column.source" }),
|
||||
sortingFn: (a, b) => {
|
||||
sortFn: (a, b) => {
|
||||
const aVal = a.original.domainNames?.[0] ?? "";
|
||||
const bVal = b.original.domainNames?.[0] ?? "";
|
||||
return aVal.localeCompare(bVal);
|
||||
@@ -143,14 +138,12 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const tableInstance = useReactTable<DeadHost>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
state: { sorting },
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
|
||||
import {
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { createColumnHelper, type SortingState, useTable } from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { ProxyHost } from "src/api/backend";
|
||||
import {
|
||||
@@ -17,6 +11,7 @@ import {
|
||||
HasPermission,
|
||||
TrueFalseFormatter,
|
||||
} from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
|
||||
@@ -31,7 +26,7 @@ interface Props {
|
||||
onNew?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
|
||||
const columnHelper = createColumnHelper<ProxyHost>();
|
||||
const columnHelper = createColumnHelper<Features, ProxyHost>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: any) => row.owner, {
|
||||
@@ -48,7 +43,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "domainNames",
|
||||
header: intl.formatMessage({ id: "column.source" }),
|
||||
sortingFn: (a, b) => {
|
||||
sortFn: (a, b) => {
|
||||
const aVal = a.original.domainNames?.[0] ?? "";
|
||||
const bVal = b.original.domainNames?.[0] ?? "";
|
||||
return aVal.localeCompare(bVal);
|
||||
@@ -61,7 +56,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "forwardHost",
|
||||
header: intl.formatMessage({ id: "column.destination" }),
|
||||
sortingFn: (a, b) => {
|
||||
sortFn: (a, b) => {
|
||||
const aVal = `${a.original.forwardHost}:${a.original.forwardPort}`;
|
||||
const bVal = `${b.original.forwardHost}:${b.original.forwardPort}`;
|
||||
return aVal.localeCompare(bVal);
|
||||
@@ -165,14 +160,12 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const tableInstance = useReactTable<ProxyHost>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
state: { sorting },
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
|
||||
import {
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { createColumnHelper, type SortingState, useTable } from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { RedirectionHost } from "src/api/backend";
|
||||
import {
|
||||
@@ -16,6 +10,7 @@ import {
|
||||
HasPermission,
|
||||
TrueFalseFormatter,
|
||||
} from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions";
|
||||
@@ -30,7 +25,7 @@ interface Props {
|
||||
onNew?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
|
||||
const columnHelper = createColumnHelper<RedirectionHost>();
|
||||
const columnHelper = createColumnHelper<Features, RedirectionHost>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: any) => row.owner, {
|
||||
@@ -47,7 +42,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "domainNames",
|
||||
header: intl.formatMessage({ id: "column.source" }),
|
||||
sortingFn: (a, b) => {
|
||||
sortFn: (a, b) => {
|
||||
const aVal = a.original.domainNames?.[0] ?? "";
|
||||
const bVal = b.original.domainNames?.[0] ?? "";
|
||||
return aVal.localeCompare(bVal);
|
||||
@@ -164,14 +159,12 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const tableInstance = useReactTable<RedirectionHost>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
state: { sorting },
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
|
||||
import {
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { createColumnHelper, type SortingState, useTable } from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { Stream } from "src/api/backend";
|
||||
import {
|
||||
@@ -16,6 +10,7 @@ import {
|
||||
TrueFalseFormatter,
|
||||
ValueWithDateFormatter,
|
||||
} from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { MANAGE, STREAMS } from "src/modules/Permissions";
|
||||
@@ -30,7 +25,7 @@ interface Props {
|
||||
onNew?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onDisableToggle, onNew }: Props) {
|
||||
const columnHelper = createColumnHelper<Stream>();
|
||||
const columnHelper = createColumnHelper<Features, Stream>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: any) => row.owner, {
|
||||
@@ -47,7 +42,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "incomingPort",
|
||||
header: intl.formatMessage({ id: "column.incoming-port" }),
|
||||
sortingFn: (a, b) => (a.original.incomingPort ?? 0) - (b.original.incomingPort ?? 0),
|
||||
sortFn: (a, b) => (a.original.incomingPort ?? 0) - (b.original.incomingPort ?? 0),
|
||||
cell: (info: any) => {
|
||||
const value = info.getValue();
|
||||
return <ValueWithDateFormatter value={value.incomingPort} createdOn={value.createdOn} />;
|
||||
@@ -56,7 +51,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
id: "forwardHttpCode",
|
||||
header: intl.formatMessage({ id: "column.destination" }),
|
||||
sortingFn: (a, b) => {
|
||||
sortFn: (a, b) => {
|
||||
const aVal = `${a.original.forwardingHost}:${a.original.forwardingPort}`;
|
||||
const bVal = `${b.original.forwardingHost}:${b.original.forwardingPort}`;
|
||||
return aVal.localeCompare(bVal);
|
||||
@@ -174,14 +169,12 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const tableInstance = useReactTable<Stream>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
state: { sorting },
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
IconShield,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { createColumnHelper, useTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { User } from "src/api/backend";
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TrueFalseFormatter,
|
||||
ValueWithDateFormatter,
|
||||
} from "src/components";
|
||||
import { type Features, features } from "src/components/Table/features";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
|
||||
@@ -47,7 +48,7 @@ export default function Table({
|
||||
onNewUser,
|
||||
onLoginAs,
|
||||
}: Props) {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
const columnHelper = createColumnHelper<Features, User>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor((row: any) => row, {
|
||||
@@ -216,11 +217,10 @@ export default function Table({
|
||||
],
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<User>({
|
||||
const tableInstance = useTable({
|
||||
features,
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
rowCount: data.length,
|
||||
meta: {
|
||||
isFetching,
|
||||
},
|
||||
|
||||
+870
-679
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -4,21 +4,21 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@cypress/grep": "^6.0.0",
|
||||
"@cypress/grep": "^6.0.3",
|
||||
"@jc21/cypress-swagger-validation": "^0.3.2",
|
||||
"@quobix/vacuum": "^0.26.4",
|
||||
"axios": "^1.16.0",
|
||||
"axios": "^1.19.0",
|
||||
"chalk": "^5.6.2",
|
||||
"cypress": "^15.15.0",
|
||||
"cypress": "^15.21.1",
|
||||
"cypress-multi-reporters": "^2.0.5",
|
||||
"cypress-wait-until": "^3.0.2",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint": "^10.9.1",
|
||||
"eslint-plugin-align-assignments": "^1.1.2",
|
||||
"eslint-plugin-chai-friendly": "^1.2.0",
|
||||
"eslint-plugin-cypress": "^6.4.1",
|
||||
"form-data": "^4.0.5",
|
||||
"eslint-plugin-chai-friendly": "^1.2.1",
|
||||
"eslint-plugin-cypress": "^7.0.1",
|
||||
"form-data": "^4.0.6",
|
||||
"lodash": "^4.18.1",
|
||||
"mocha": "^11.7.5",
|
||||
"mocha": "^11.8.0",
|
||||
"mocha-junit-reporter": "^2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
+103
-48
@@ -80,10 +80,10 @@
|
||||
"@babel/helper-string-parser" "^7.29.7"
|
||||
"@babel/helper-validator-identifier" "^7.29.7"
|
||||
|
||||
"@cypress/grep@^6.0.0":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/grep/-/grep-6.0.1.tgz#33fb03fd860f3cc4d3f4940c54ad2f6426d0dfea"
|
||||
integrity sha512-nxTBNid7mKeDfM5thNPc4YH6q+1BoDKFwDLcJJLvosNbJ9WcsRZAU9F6gUQyS/BWhKmRWn+st3JdVtokk8aMSw==
|
||||
"@cypress/grep@^6.0.3":
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/grep/-/grep-6.0.3.tgz#c8023bbf996e406fe932e5861674ded0499e8fda"
|
||||
integrity sha512-9pm3HVlbXHp9K9XU3hrnY7OBXsCGKGeCZ49n7yOrXbBlPv6FdSq3ub1KDJQ0AvtIi6A/3zCzwU0uZzq9skG6Ag==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
find-test-names "^1.28.18"
|
||||
@@ -141,10 +141,10 @@
|
||||
debug "^4.3.1"
|
||||
minimatch "^10.2.4"
|
||||
|
||||
"@eslint/config-helpers@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz#ef9a36881d39dfd5dbeac22b0da997fabfb08b03"
|
||||
integrity sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==
|
||||
"@eslint/config-helpers@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.7.0.tgz#09ee4aa07b73f059ec2d4c74bf4b2ff02b322377"
|
||||
integrity sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==
|
||||
dependencies:
|
||||
"@eslint/core" "^1.2.1"
|
||||
|
||||
@@ -386,10 +386,10 @@ ansi-styles@^6.1.0, ansi-styles@^6.2.1, ansi-styles@^6.2.3:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041"
|
||||
integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==
|
||||
|
||||
arch@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
|
||||
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
|
||||
arch@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/arch/-/arch-3.0.0.tgz#a44e7077da4615fc5f1e3da21fbfc201d2c1817c"
|
||||
integrity sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q==
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -433,7 +433,17 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef"
|
||||
integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==
|
||||
|
||||
axios@^1.16.0, axios@^1.7.7:
|
||||
axios@^1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.19.0.tgz#ddf864d4c8233c0e6873746ab59361537d05ad39"
|
||||
integrity sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==
|
||||
dependencies:
|
||||
follow-redirects "^1.16.0"
|
||||
form-data "^4.0.6"
|
||||
https-proxy-agent "^5.0.1"
|
||||
proxy-from-env "^2.1.0"
|
||||
|
||||
axios@^1.7.7:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12"
|
||||
integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==
|
||||
@@ -489,6 +499,13 @@ brace-expansion@^5.0.5:
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
brace-expansion@^5.0.8:
|
||||
version "5.0.9"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf"
|
||||
integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
braces@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
@@ -580,6 +597,14 @@ chownr@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4"
|
||||
integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
|
||||
|
||||
chrome-remote-interface@0.33.3:
|
||||
version "0.33.3"
|
||||
resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz#d8b4339f487b460a9461af7355bda98054e8e1a4"
|
||||
integrity sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==
|
||||
dependencies:
|
||||
commander "2.11.x"
|
||||
ws "^7.2.0"
|
||||
|
||||
ci-info@^4.1.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c"
|
||||
@@ -647,6 +672,11 @@ combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@2.11.x:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
|
||||
integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==
|
||||
|
||||
commander@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||
@@ -690,22 +720,23 @@ cypress-wait-until@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-3.0.2.tgz#c90dddfa4c46a2c422f5b91d486531c560bae46e"
|
||||
integrity sha512-iemies796dD5CgjG5kV0MnpEmKSH+s7O83ZoJLVzuVbZmm4lheMsZqAVT73hlMx4QlkwhxbyUzhOBUOZwoOe0w==
|
||||
|
||||
cypress@^15.15.0:
|
||||
version "15.16.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-15.16.0.tgz#482f77e6f85aee98b94a5ad844d36f69dc212c28"
|
||||
integrity sha512-fy0M0c9xDLEp4v9y7LLKFeAQhIdDsobxDSKpD3JcZpqQefjy9TSzEyVV3HA0zu7hUi0bGHlSYlI7ASub8wgR9A==
|
||||
cypress@^15.21.1:
|
||||
version "15.21.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-15.21.1.tgz#51653d58fa0e9994a11406aa3df2bee716264f9a"
|
||||
integrity sha512-ogHpHMj0XNlZA5MGzjg8SWHf0eMw9lTwVQRKjpcT1GzNTxNUjwnHA8YCG6nOJ8ThU+XZaUxJgpMYZnJ//8aDaw==
|
||||
dependencies:
|
||||
"@cypress/request" "^4.0.0"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
"@types/sinonjs__fake-timers" "8.1.1"
|
||||
"@types/sizzle" "^2.3.2"
|
||||
"@types/tmp" "^0.2.3"
|
||||
arch "^2.2.0"
|
||||
arch "^3.0.0"
|
||||
blob-util "^2.0.2"
|
||||
bluebird "^3.7.2"
|
||||
buffer "^5.7.1"
|
||||
cachedir "^2.4.0"
|
||||
chalk "^4.1.0"
|
||||
chrome-remote-interface "0.33.3"
|
||||
ci-info "^4.1.0"
|
||||
cli-table3 "0.6.1"
|
||||
commander "^6.2.1"
|
||||
@@ -731,7 +762,6 @@ cypress@^15.15.0:
|
||||
systeminformation "^5.31.1"
|
||||
tmp "~0.2.4"
|
||||
tree-kill "1.2.2"
|
||||
tslib "1.14.1"
|
||||
untildify "^4.0.0"
|
||||
yauzl "^3.3.1"
|
||||
|
||||
@@ -895,17 +925,17 @@ eslint-plugin-align-assignments@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-align-assignments/-/eslint-plugin-align-assignments-1.1.2.tgz#83e1a8a826d4adf29e82b52d0bb39c88b301b576"
|
||||
integrity sha512-I1ZJgk9EjHfGVU9M2Ex8UkVkkjLL5Y9BS6VNnQHq79eHj2H4/Cgxf36lQSUTLgm2ntB03A2NtF+zg9fyi5vChg==
|
||||
|
||||
eslint-plugin-chai-friendly@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.2.0.tgz#2a54b481bcbc26ab5a581b2b7e7c304465837c15"
|
||||
integrity sha512-um2pBb4ZXNCoTRPRAWiUaXeIaw1dRaPOEZ+G/qcZqfyTdkCXXwOBctnfnbIRbZiQf4AXl3ImV1grt423SlK+mg==
|
||||
eslint-plugin-chai-friendly@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.2.1.tgz#141392762aa8626ac3d25780a502e625ce74922b"
|
||||
integrity sha512-mV3EOJLDr8+L+LS8uCkP711fnNHz+4PsmPyz18xwkvjJwfLRlnx0Eu6CFnb5B+dW5ahoav2jVer2KFYsmIuv3A==
|
||||
|
||||
eslint-plugin-cypress@^6.4.1:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-6.4.1.tgz#2aa64e5a2dcc56b8b916fdf8398a81b31a61c462"
|
||||
integrity sha512-8mnfR3q0Lr41Fu9SYZGeZ5nbSBgtS44+bbtSs7k0KvfoQ2pPmK43IvaTP6FGTfwBTN6S7w027CpqjrLAd65AYA==
|
||||
eslint-plugin-cypress@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-7.0.1.tgz#eb80500478533204ad5ebd2e49944224b0e47941"
|
||||
integrity sha512-Jn2sV9rt8iiUYZY+iGY8fvV8iseJ6Tl1jngnLeSRyuHvYgxEbDUUFwNhwj1cr9gI4xCzTDh4mgXJ3YY7+dEMVg==
|
||||
dependencies:
|
||||
globals "^17.6.0"
|
||||
globals "^17.11.0"
|
||||
|
||||
eslint-scope@^9.1.2:
|
||||
version "9.1.2"
|
||||
@@ -927,15 +957,15 @@ eslint-visitor-keys@^5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
|
||||
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
|
||||
|
||||
eslint@^10.3.0:
|
||||
version "10.4.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.1.tgz#f6640b176e0912246d9ddbf8fcfa5e8b7f02445a"
|
||||
integrity sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==
|
||||
eslint@^10.9.1:
|
||||
version "10.9.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.9.1.tgz#409da5c41a5536d5a849f8555a18ca7ef1eb963b"
|
||||
integrity sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@eslint/config-array" "^0.23.5"
|
||||
"@eslint/config-helpers" "^0.6.0"
|
||||
"@eslint/config-helpers" "^0.7.0"
|
||||
"@eslint/core" "^1.2.1"
|
||||
"@eslint/plugin-kit" "^0.7.2"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
@@ -959,7 +989,7 @@ eslint@^10.3.0:
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
minimatch "^10.2.4"
|
||||
minimatch "^10.2.5"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
|
||||
@@ -1185,6 +1215,17 @@ form-data@^4.0.5, form-data@~4.0.4:
|
||||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827"
|
||||
integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
hasown "^2.0.4"
|
||||
mime-types "^2.1.35"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||
@@ -1288,10 +1329,10 @@ global-dirs@^3.0.0:
|
||||
dependencies:
|
||||
ini "2.0.0"
|
||||
|
||||
globals@^17.6.0:
|
||||
version "17.6.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.6.0.tgz#0f0be018d5cca8690e6375ead1f65c4bb96191fc"
|
||||
integrity sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==
|
||||
globals@^17.11.0:
|
||||
version "17.11.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.11.0.tgz#d643485bb30220d7751e511cf4f68c73d3870d87"
|
||||
integrity sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==
|
||||
|
||||
globby@^11.0.4:
|
||||
version "11.1.0"
|
||||
@@ -1347,6 +1388,13 @@ hasown@^2.0.2:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
hasown@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003"
|
||||
integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
he@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
@@ -1663,7 +1711,7 @@ mime-db@1.52.0:
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@~2.1.19:
|
||||
mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.19:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
@@ -1687,6 +1735,13 @@ minimatch@^10.2.4:
|
||||
dependencies:
|
||||
brace-expansion "^5.0.5"
|
||||
|
||||
minimatch@^10.2.5:
|
||||
version "10.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef"
|
||||
integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.8"
|
||||
|
||||
minimatch@^9.0.4, minimatch@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
||||
@@ -1727,10 +1782,10 @@ mocha-junit-reporter@^2.2.1:
|
||||
strip-ansi "^6.0.1"
|
||||
xml "^1.0.1"
|
||||
|
||||
mocha@^11.7.5:
|
||||
version "11.7.6"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.6.tgz#ebbe22989d04cbb9424a36307320476624c41a33"
|
||||
integrity sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==
|
||||
mocha@^11.8.0:
|
||||
version "11.8.0"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.8.0.tgz#9163cc59600ec26470f6b040e30aa51acc973ed2"
|
||||
integrity sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==
|
||||
dependencies:
|
||||
browser-stdout "^1.3.1"
|
||||
chokidar "^4.0.1"
|
||||
@@ -2307,11 +2362,6 @@ tree-kill@1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
tslib@1.14.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
@@ -2430,6 +2480,11 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@^7.2.0:
|
||||
version "7.5.13"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.13.tgz#12aa507eaca76c295c278b1aebf4698ab2c1845f"
|
||||
integrity sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==
|
||||
|
||||
xml@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
|
||||
|
||||
Reference in New Issue
Block a user