Merge branch 'develop' into claude/nifty-neumann-83e87c

This commit is contained in:
Timothé Stoifl
2026-08-22 15:40:15 +02:00
committed by GitHub
63 changed files with 1897 additions and 1793 deletions
+72 -89
View File
@@ -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"
}
}
}
}
+4 -4
View File
@@ -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
View File
@@ -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");
+21 -34
View File
@@ -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;
+1 -6
View File
@@ -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(", ")}]`);
+19 -35
View File
@@ -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,11 +162,10 @@ 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);
@@ -224,18 +217,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 +249,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 +263,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 +288,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 +299,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 +326,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)
+4 -1
View File
@@ -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";
}
+4 -14
View File
@@ -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
+3 -5
View File
@@ -257,11 +257,9 @@ const internalUser = {
},
deleteAll: async () => {
await userModel
.query()
.patch({
is_deleted: 1,
});
await userModel.query().patch({
is_deleted: 1,
});
},
/**
+11 -11
View File
@@ -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",
},
},
};
+1 -4
View File
@@ -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);
+36 -16
View File
@@ -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(),
};
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import _ from "lodash";
import _ from "lodash";
export default (default_sort, default_offset, default_limit, max_limit) => {
/**
+2 -2
View File
@@ -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);
}
+1 -5
View File
@@ -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 -1
View File
@@ -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 };
+8 -8
View File
@@ -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"];
}
}
+7 -7
View File
@@ -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
View File
@@ -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": {
+1 -4
View File
@@ -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,
},
);
+7 -29
View File
@@ -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, {
+10 -41
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+73 -90
View File
@@ -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"
}
}
}
}
+25 -25
View File
@@ -17,50 +17,50 @@
},
"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-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"
}
}
+6 -5
View File
@@ -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>
))}
@@ -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">
+4 -1
View File
@@ -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">
+8 -8
View File
@@ -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">
+5 -4
View File
@@ -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}>
+2 -2
View File
@@ -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">
+35
View File
@@ -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
View File
@@ -1,4 +1,5 @@
export * from "./Formatter";
export * from "./features";
export * from "./TableHeader";
export * from "./TableHelpers";
export * from "./TableLayout";
+82 -82
View File
@@ -27,73 +27,73 @@ import langList from "./lang/lang-list.json";
// 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],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
const thisLocale = (locale || "en").slice(0, 2);
const thisLocale = (locale || "en").slice(0, 2);
// ensure this lang exists in localeOptions above, otherwise fallback to en
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
return Object.assign({}, langList, langEn);
}
// ensure this lang exists in localeOptions above, otherwise fallback to en
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
return Object.assign({}, langList, langEn);
}
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
};
const getFlagCodeForLocale = (locale?: string) => {
const thisLocale = (locale || "en").slice(0, 2);
const thisLocale = (locale || "en").slice(0, 2);
// only add to this if your flag is different from the locale code
const specialCases: Record<string, string> = {
ja: "jp", // Japan
zh: "cn", // China
vi: "vn", // Vietnam
ko: "kr", // Korea
cs: "cz", // Czechia
ga: "ie", // Ireland (Irish)
};
// only add to this if your flag is different from the locale code
const specialCases: Record<string, string> = {
ja: "jp", // Japan
zh: "cn", // China
vi: "vn", // Vietnam
ko: "kr", // Korea
cs: "cz", // Czechia
ga: "ie", // Ireland (Irish)
};
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();
@@ -102,41 +102,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 -6
View File
@@ -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;
+1 -1
View File
@@ -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";
+8 -9
View File
@@ -22,18 +22,17 @@ import * as hu from "./hu/index";
const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu };
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";
+1 -5
View File
@@ -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>
+1 -5
View File
@@ -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>
+21 -7
View File
@@ -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">
+3 -1
View File
@@ -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 ? (
+2 -13
View File
@@ -3,13 +3,7 @@ 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 { 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";
@@ -137,12 +131,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>
) : (
+5 -5
View File
@@ -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,
},
+7 -7
View File
@@ -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,
},
+5 -5
View File
@@ -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,
},
+1 -3
View File
@@ -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>
+6 -13
View File
@@ -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,
},
+7 -14
View File
@@ -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,
},
+7 -14
View File
@@ -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,
},
+5 -5
View File
@@ -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,
},
+857 -679
View File
File diff suppressed because it is too large Load Diff