Updated backend packages

This commit is contained in:
Jamie Curnow
2026-08-22 23:10:26 +10:00
parent a62c2a6dc3
commit 4e2b052b50
27 changed files with 676 additions and 742 deletions
+72 -89
View File
@@ -1,91 +1,74 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",
"useIgnoreFile": true "useIgnoreFile": true
}, },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"includes": [ "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"]
"**/*.ts", },
"**/*.tsx", "formatter": {
"**/*.js", "enabled": true,
"**/*.jsx", "indentStyle": "tab",
"!**/dist/**/*" "indentWidth": 4,
] "lineWidth": 120,
}, "formatWithErrors": true
"formatter": { },
"enabled": true, "assist": {
"indentStyle": "tab", "actions": {
"indentWidth": 4, "source": {
"lineWidth": 120, "organizeImports": {
"formatWithErrors": true "level": "on",
}, "options": {
"assist": { "groups": [
"actions": { ":BUN:",
"source": { ":NODE:",
"organizeImports": { ["npm:*", "npm:*/**"],
"level": "on", ":PACKAGE_WITH_PROTOCOL:",
"options": { ":URL:",
"groups": [ ":PACKAGE:",
":BUN:", ["/src/*", "/src/**"],
":NODE:", ["/**"],
[ ["#*", "#*/**"],
"npm:*", ":PATH:"
"npm:*/**" ]
], }
":PACKAGE_WITH_PROTOCOL:", }
":URL:", }
":PACKAGE:", }
[ },
"/src/*", "linter": {
"/src/**" "enabled": true,
], "rules": {
[ "preset": "recommended",
"/**" "correctness": {
], "useUniqueElementIds": "off"
[ },
"#*", "suspicious": {
"#*/**" "noExplicitAny": "off"
], },
":PATH:" "performance": {
] "noDelete": "off"
} },
} "nursery": "off",
} "a11y": {
} "useSemanticElements": "off",
}, "useValidAnchor": "off"
"linter": { },
"enabled": true, "style": {
"rules": { "noParameterAssign": "error",
"recommended": true, "useAsConstAssertion": "error",
"correctness": { "useDefaultParameterLast": "error",
"useUniqueElementIds": "off" "useEnumInitializers": "error",
}, "useSelfClosingElements": "error",
"suspicious": { "useSingleVarDeclarator": "error",
"noExplicitAny": "off" "noUnusedTemplateLiteral": "error",
}, "useNumberNamespace": "error",
"performance": { "noInferrableTypes": "error",
"noDelete": "off" "noUselessElse": "error"
}, }
"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 knex from "knex";
import {configGet, configHas} from "./lib/config.js"; import { configGet, configHas } from "./lib/config.js";
let instance = null; let instance = null;
@@ -23,8 +23,8 @@ const generateDbConfig = () => {
user: cfg.user, user: cfg.user,
password: cfg.password, password: cfg.password,
database: cfg.name, database: cfg.name,
port: cfg.port, port: cfg.port,
...(cfg.ssl ? { ssl: cfg.ssl } : {}) ...(cfg.ssl ? { ssl: cfg.ssl } : {}),
}, },
migrations: { migrations: {
tableName: "migrations", tableName: "migrations",
@@ -37,6 +37,6 @@ const getInstance = () => {
instance = knex(generateDbConfig()); instance = knex(generateDbConfig());
} }
return instance; return instance;
} };
export default getInstance; export default getInstance;
+7 -11
View File
@@ -161,12 +161,12 @@ const internal2fa = {
} }
const result = await verify({ const result = await verify({
token: code, token: code,
secret: auth.meta.totp_secret, secret: auth.meta.totp_secret,
guardrails: createGuardrails({ guardrails: createGuardrails({
MIN_SECRET_BYTES: 10, MIN_SECRET_BYTES: 10,
}), }),
}); });
if (!result.valid) { if (!result.valid) {
throw new errs.AuthError("Invalid verification code"); throw new errs.AuthError("Invalid verification code");
@@ -288,11 +288,7 @@ const internal2fa = {
}, },
getUserPasswordAuth: async (userId) => { getUserPasswordAuth: async (userId) => {
const auth = await authModel const auth = await authModel.query().where("user_id", userId).andWhere("type", "password").first();
.query()
.where("user_id", userId)
.andWhere("type", "password")
.first();
if (!auth) { if (!auth) {
throw new errs.ItemNotFoundError("Auth not found"); throw new errs.ItemNotFoundError("Auth not found");
+21 -34
View File
@@ -66,7 +66,7 @@ const internalAccessList = {
id: data.id, id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"], expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
}, },
true // skip masking true, // skip masking
); );
// Audit log // Audit log
@@ -180,10 +180,10 @@ const internalAccessList = {
id: data.id, id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"], 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)) { if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts); await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
} }
@@ -202,17 +202,13 @@ const internalAccessList = {
*/ */
get: async (access, data, skipMasking) => { get: async (access, data, skipMasking) => {
const thisData = data || {}; 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 const query = accessListModel
.query() .query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count")) .select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () { .leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn( this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
"proxy_host.is_deleted",
"=",
0,
);
}) })
.where("access_list.is_deleted", 0) .where("access_list.is_deleted", 0)
.andWhere("access_list.id", thisData.id) .andWhere("access_list.id", thisData.id)
@@ -267,19 +263,13 @@ const internalAccessList = {
// 4. audit log // 4. audit log
// 1. update row to be deleted // 1. update row to be deleted
await accessListModel await accessListModel.query().where("id", row.id).patch({
.query() is_deleted: 1,
.where("id", row.id) });
.patch({
is_deleted: 1,
});
// 2. update any proxy hosts that were using it (ignoring permissions) // 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) { if (row.proxy_hosts) {
await proxyHostModel await proxyHostModel.query().where("access_list_id", "=", row.id).patch({ access_list_id: 0 });
.query()
.where("access_list_id", "=", row.id)
.patch({ access_list_id: 0 });
// 3. reconfigure those hosts, then reload nginx // 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items // set the access_list_id to zero for these items
@@ -325,11 +315,7 @@ const internalAccessList = {
.query() .query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count")) .select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () { .leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn( this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
"proxy_host.is_deleted",
"=",
0,
);
}) })
.where("access_list.is_deleted", 0) .where("access_list.is_deleted", 0)
.groupBy("access_list.id") .groupBy("access_list.id")
@@ -371,10 +357,7 @@ const internalAccessList = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: async (userId, visibility) => { getCount: async (userId, visibility) => {
const query = accessListModel const query = accessListModel.query().count("id as count").where("is_deleted", 0);
.query()
.count("id as count")
.where("is_deleted", 0);
if (visibility !== "all") { if (visibility !== "all") {
query.andWhere("owner_user_id", userId); query.andWhere("owner_user_id", userId);
@@ -436,20 +419,24 @@ const internalAccessList = {
} }
// 2. create empty access file // 2. create empty access file
fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'}); fs.writeFileSync(htpasswdFile, "", { encoding: "utf8" });
// 3. generate password for each user // 3. generate password for each user
if (list.items.length) { if (list.items.length) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
batchflow(list.items).sequential() batchflow(list.items)
.sequential()
.each((_i, item, next) => { .each((_i, item, next) => {
if (item.password?.length) { if (item.password?.length) {
logger.info(`Adding: ${item.username}`); logger.info(`Adding: ${item.username}`);
utils.execFile('openssl', ['passwd', '-apr1', item.password]) utils
.execFile("openssl", ["passwd", "-apr1", item.password])
.then((res) => { .then((res) => {
try { try {
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'}); fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {
encoding: "utf8",
});
} catch (err) { } catch (err) {
reject(err); reject(err);
} }
@@ -471,7 +458,7 @@ const internalAccessList = {
}); });
}); });
} }
} },
} };
export default 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"; import auditLogModel from "../models/audit-log.js";
const internalAuditLog = { const internalAuditLog = {
/** /**
* All logs * All logs
* *
@@ -46,11 +45,7 @@ const internalAuditLog = {
get: async (access, data) => { get: async (access, data) => {
await access.can("auditlog:list"); await access.can("auditlog:list");
const query = auditLogModel const query = auditLogModel.query().andWhere("id", data.id).allowGraph("[user]").first();
.query()
.andWhere("id", data.id)
.allowGraph("[user]")
.first();
if (typeof data.expand !== "undefined" && data.expand !== null) { if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`); query.withGraphFetched(`[${data.expand.join(", ")}]`);
+19 -35
View File
@@ -54,9 +54,7 @@ const internalDeadHost = {
thisData.advanced_config = ""; thisData.advanced_config = "";
} }
const row = await deadHostModel.query() const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
.insertAndFetch(thisData)
.then(utils.omitRow(omissions()));
// Add to audit log // Add to audit log
await internalAuditLog.add(access, { await internalAuditLog.add(access, {
@@ -153,12 +151,8 @@ const internalDeadHost = {
thisData = internalHost.cleanSslHstsData(thisData, row); thisData = internalHost.cleanSslHstsData(thisData, row);
// do the row update // do the row update
await deadHostModel await deadHostModel.query().where({ id: data.id }).patch(data);
.query()
.where({id: data.id})
.patch(data);
// Add to audit log // Add to audit log
await internalAuditLog.add(access, { await internalAuditLog.add(access, {
@@ -168,11 +162,10 @@ const internalDeadHost = {
meta: thisData, meta: thisData,
}); });
const thisRow = await internalDeadHost const thisRow = await internalDeadHost.get(access, {
.get(access, { id: thisData.id,
id: thisData.id, expand: ["owner", "certificate"],
expand: ["owner", "certificate"], });
});
// Configure nginx // Configure nginx
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row); const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row);
@@ -224,18 +217,15 @@ const internalDeadHost = {
* @returns {Promise} * @returns {Promise}
*/ */
delete: async (access, data) => { 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 }); const row = await internalDeadHost.get(access, { id: data.id });
if (!row?.id) { if (!row?.id) {
throw new errs.ItemNotFoundError(data.id); throw new errs.ItemNotFoundError(data.id);
} }
await deadHostModel await deadHostModel.query().where("id", row.id).patch({
.query() is_deleted: 1,
.where("id", row.id) });
.patch({
is_deleted: 1,
});
// Delete Nginx Config // Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row); await internalNginx.deleteConfig("dead_host", row);
@@ -259,7 +249,7 @@ const internalDeadHost = {
* @returns {Promise} * @returns {Promise}
*/ */
enable: async (access, data) => { 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, { const row = await internalDeadHost.get(access, {
id: data.id, id: data.id,
expand: ["certificate", "owner"], expand: ["certificate", "owner"],
@@ -273,12 +263,9 @@ const internalDeadHost = {
row.enabled = 1; row.enabled = 1;
await deadHostModel await deadHostModel.query().where("id", row.id).patch({
.query() enabled: 1,
.where("id", row.id) });
.patch({
enabled: 1,
});
// Configure nginx // Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", row); await internalNginx.configure(deadHostModel, "dead_host", row);
@@ -301,7 +288,7 @@ const internalDeadHost = {
* @returns {Promise} * @returns {Promise}
*/ */
disable: async (access, data) => { 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 }); const row = await internalDeadHost.get(access, { id: data.id });
if (!row?.id) { if (!row?.id) {
throw new errs.ItemNotFoundError(data.id); throw new errs.ItemNotFoundError(data.id);
@@ -312,12 +299,9 @@ const internalDeadHost = {
row.enabled = 0; row.enabled = 0;
await deadHostModel await deadHostModel.query().where("id", row.id).patch({
.query() enabled: 0,
.where("id", row.id) });
.patch({
enabled: 0,
});
// Delete Nginx Config // Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row); await internalNginx.deleteConfig("dead_host", row);
@@ -342,7 +326,7 @@ const internalDeadHost = {
* @returns {Promise} * @returns {Promise}
*/ */
getAll: async (access, expand, searchQuery) => { getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("dead_hosts:list") const accessData = await access.can("dead_hosts:list");
const query = deadHostModel const query = deadHostModel
.query() .query()
.where("is_deleted", 0) .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 // 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"; host.forward_scheme = "$scheme";
} }
+4 -14
View File
@@ -38,11 +38,7 @@ export default {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
} }
const auth = await authModel const auth = await authModel.query().where("user_id", "=", user.id).where("type", "=", "password").first();
.query()
.where("user_id", "=", user.id)
.where("type", "=", "password")
.first();
if (!auth) { if (!auth) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
@@ -50,10 +46,7 @@ export default {
const valid = await auth.verifyPassword(data.secret); const valid = await auth.verifyPassword(data.secret);
if (!valid) { if (!valid) {
throw new errs.AuthError( throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH, ERROR_MESSAGE_INVALID_AUTH_I18N);
ERROR_MESSAGE_INVALID_AUTH,
ERROR_MESSAGE_INVALID_AUTH_I18N,
);
} }
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) { if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
@@ -171,7 +164,7 @@ export default {
} }
// Check scope // Check scope
if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") { if (tokenData.scope?.[0] !== "2fa-challenge") {
throw new errs.AuthError("Invalid challenge token"); throw new errs.AuthError("Invalid challenge token");
} }
@@ -183,10 +176,7 @@ export default {
// Verify 2FA code // Verify 2FA code
const valid = await twoFactor.verifyForLogin(userId, code); const valid = await twoFactor.verifyForLogin(userId, code);
if (!valid) { if (!valid) {
throw new errs.AuthError( throw new errs.AuthError(ERROR_MESSAGE_INVALID_2FA, ERROR_MESSAGE_INVALID_2FA_I18N);
ERROR_MESSAGE_INVALID_2FA,
ERROR_MESSAGE_INVALID_2FA_I18N,
);
} }
// Create full token // Create full token
+3 -5
View File
@@ -257,11 +257,9 @@ const internalUser = {
}, },
deleteAll: async () => { deleteAll: async () => {
await userModel await userModel.query().patch({
.query() is_deleted: 1,
.patch({ });
is_deleted: 1,
});
}, },
/** /**
+11 -11
View File
@@ -1,19 +1,19 @@
module.exports = { module.exports = {
development: { development: {
client: 'mysql2', client: "mysql2",
migrations: { migrations: {
tableName: 'migrations', tableName: "migrations",
stub: 'lib/migrate_template.js', stub: "lib/migrate_template.js",
directory: 'migrations' directory: "migrations",
} },
}, },
production: { production: {
client: 'mysql2', client: "mysql2",
migrations: { migrations: {
tableName: 'migrations', tableName: "migrations",
stub: 'lib/migrate_template.js', stub: "lib/migrate_template.js",
directory: 'migrations' directory: "migrations",
} },
} },
}; };
+1 -4
View File
@@ -119,10 +119,7 @@ export default function (tokenString) {
// Proxy Hosts // Proxy Hosts
case "proxy_hosts": { case "proxy_hosts": {
const query = proxyHostModel const query = proxyHostModel.query().select("id").andWhere("is_deleted", 0);
.query()
.select("id")
.andWhere("is_deleted", 0);
if (permissions.visibility === "user") { if (permissions.visibility === "user") {
query.andWhere("owner_user_id", tokenUserId); 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 NodeRSA from "node-rsa";
import { global as logger } from "../logger.js"; import { global as logger } from "../logger.js";
const keysFile = '/data/keys.json'; const keysFile = "/data/keys.json";
const mysqlEngine = 'mysql2'; const mysqlEngine = "mysql2";
const postgresEngine = 'pg'; const postgresEngine = "pg";
const sqliteClientName = 'better-sqlite3'; const sqliteClientName = "better-sqlite3";
// Not used for new setups anymore but may exist in legacy setups // Not used for new setups anymore but may exist in legacy setups
const legacySqliteClientName = 'sqlite3'; const legacySqliteClientName = "sqlite3";
let instance = null; 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 envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null; const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null; const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL); 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 envMysqlSSLRejectUnauthorized =
const envMysqlSSLVerifyIdentity = process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY); 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) { if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql // we have enough mysql creds to go with mysql
logger.info("Using MySQL configuration"); logger.info("Using MySQL configuration");
@@ -58,8 +64,10 @@ const configure = () => {
port: process.env.DB_MYSQL_PORT || 3306, port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser, user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD, password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName, name: envMysqlName,
ssl: envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false, ssl: envMysqlSSL
? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity }
: false,
}, },
keys: getKeys(), keys: getKeys(),
}; };
@@ -222,7 +230,7 @@ const isDebugMode = () => !!process.env.DEBUG;
* *
* @returns {boolean} * @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 * Returns a public key
@@ -259,4 +267,16 @@ const useLetsencryptServer = () => {
return null; 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) => { export default (default_sort, default_offset, default_limit, max_limit) => {
/** /**
+2 -2
View File
@@ -1,6 +1,6 @@
export default (req, res, next) => { export default (req, res, next) => {
if (req.params.user_id === 'me' && res.locals.access) { if (req.params.user_id === "me" && res.locals.access) {
req.params.user_id = res.locals.access.token.get('attrs').id; req.params.user_id = res.locals.access.token.get("attrs").id;
} else { } else {
req.params.user_id = Number.parseInt(req.params.user_id, 10); 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"); throw new errs.ValidationError("Payload is undefined");
} }
const validate = ajv.compile(schema); const validate = ajv.compile(schema);
const valid = validate(payload); const valid = validate(payload);
if (valid && !validate.errors) { if (valid && !validate.errors) {
return payload; return payload;
} }
const message = ajv.errorsText(validate.errors); const message = ajv.errorsText(validate.errors);
const err = new errs.ValidationError(message); const err = new errs.ValidationError(message);
err.debug = {validationErrors: validate.errors, payload}; err.debug = { validationErrors: validate.errors, payload };
throw err; 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 _ from "lodash";
import commonDefinitions from "../../schema/common.json" with { type: "json" }; import commonDefinitions from "../../schema/common.json" with { type: "json" };
import errs from "../error.js"; import errs from "../error.js";
@@ -13,13 +13,14 @@ const migrateName = "settings";
const up = (knex) => { const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`); logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.createTable('setting', (table) => { return knex.schema
table.string('id').notNull().primary(); .createTable("setting", (table) => {
table.string('name', 100).notNull(); table.string("id").notNull().primary();
table.string('description', 255).notNull(); table.string("name", 100).notNull();
table.string('value', 255).notNull(); table.string("description", 255).notNull();
table.json('meta').notNull(); table.string("value", 255).notNull();
}) table.json("meta").notNull();
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] setting Table created`); logger.info(`[${migrateName}] setting Table created`);
}); });
@@ -17,9 +17,7 @@ const up = (knex) => {
.table("redirection_host", async (table) => { .table("redirection_host", async (table) => {
// change the column default from $scheme to auto // change the column default from $scheme to auto
await table.string("forward_scheme").notNull().defaultTo("auto").alter(); await table.string("forward_scheme").notNull().defaultTo("auto").alter();
await knex('redirection_host') await knex("redirection_host").where("forward_scheme", "$scheme").update({ forward_scheme: "auto" });
.where('forward_scheme', '$scheme')
.update({ forward_scheme: 'auto' });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`); logger.info(`[${migrateName}] redirection_host Table altered`);
@@ -38,9 +36,7 @@ const down = (knex) => {
return knex.schema return knex.schema
.table("redirection_host", async (table) => { .table("redirection_host", async (table) => {
await table.string("forward_scheme").notNull().defaultTo("$scheme").alter(); await table.string("forward_scheme").notNull().defaultTo("$scheme").alter();
await knex('redirection_host') await knex("redirection_host").where("forward_scheme", "auto").update({ forward_scheme: "$scheme" });
.where('forward_scheme', 'auto')
.update({ forward_scheme: '$scheme' });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`); logger.info(`[${migrateName}] redirection_host Table altered`);
@@ -11,15 +11,15 @@ const migrateName = "trust_forwarded_proto";
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`); logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema return knex.schema
.alterTable('proxy_host', (table) => { .alterTable("proxy_host", (table) => {
table.tinyint('trust_forwarded_proto').notNullable().defaultTo(0); table.tinyint("trust_forwarded_proto").notNullable().defaultTo(0);
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info(`[${migrateName}] proxy_host Table altered`);
}); });
}; };
/** /**
@@ -29,15 +29,15 @@ const up = (knex) => {
* @returns {Promise} * @returns {Promise}
*/ */
const down = (knex) => { const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`); logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema return knex.schema
.alterTable('proxy_host', (table) => { .alterTable("proxy_host", (table) => {
table.dropColumn('trust_forwarded_proto'); table.dropColumn("trust_forwarded_proto");
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); 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()); Model.knex(db());
class Setting extends Model { class Setting extends Model {
$beforeInsert () { $beforeInsert() {
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === "undefined") {
this.meta = {}; this.meta = {};
} }
} }
static get name () { static get name() {
return 'Setting'; return "Setting";
} }
static get tableName () { static get tableName() {
return 'setting'; return "setting";
} }
static get jsonAttributes () { static get jsonAttributes() {
return ['meta']; return ["meta"];
} }
} }
+7 -7
View File
@@ -8,21 +8,21 @@ import now from "./now_helper.js";
Model.knex(db()); Model.knex(db());
class UserPermission extends Model { class UserPermission extends Model {
$beforeInsert () { $beforeInsert() {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
} }
$beforeUpdate () { $beforeUpdate() {
this.modified_on = now(); this.modified_on = now();
} }
static get name () { static get name() {
return 'UserPermission'; return "UserPermission";
} }
static get tableName () { static get tableName() {
return 'user_permission'; return "user_permission";
} }
} }
+10 -10
View File
@@ -13,37 +13,37 @@
"regenerate-config": "node scripts/regenerate-config" "regenerate-config": "node scripts/regenerate-config"
}, },
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^15.3.5", "@apidevtools/json-schema-ref-parser": "^16.0.0",
"ajv": "^8.20.0", "ajv": "^8.20.0",
"archiver": "^8.0.0", "archiver": "^8.0.0",
"batchflow": "^0.4.0", "batchflow": "^0.4.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-sqlite3": "^12.10.0", "better-sqlite3": "^13.0.3",
"body-parser": "^2.2.2", "body-parser": "^2.3.0",
"chalk": "5.6.2", "chalk": "5.6.2",
"compression": "^1.8.1", "compression": "^1.8.1",
"express": "^5.2.1", "express": "^5.2.1",
"express-fileupload": "^1.5.2", "express-fileupload": "^1.5.2",
"gravatar": "^1.8.2", "gravatar": "^1.8.2",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"knex": "3.2.10", "knex": "3.3.0",
"liquidjs": "10.27.0", "liquidjs": "10.29.0",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"mysql2": "^3.22.3", "mysql2": "^3.23.4",
"node-rsa": "^2.0.0", "node-rsa": "^2.0.0",
"objection": "3.1.5", "objection": "3.1.5",
"otplib": "^13.4.0", "otplib": "^13.5.0",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^8.21.0", "pg": "^8.23.0",
"proxy-agent": "^8.0.1", "proxy-agent": "^8.0.2",
"signale": "1.4.0", "signale": "1.4.0",
"sqlite3": "^6.0.1", "sqlite3": "^6.0.1",
"temp-write": "^6.0.1" "temp-write": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@apidevtools/swagger-parser": "^12.1.0", "@apidevtools/swagger-parser": "^12.1.0",
"@biomejs/biome": "^2.4.15", "@biomejs/biome": "^2.5.10",
"nodemon": "^3.1.14" "nodemon": "^3.1.14"
}, },
"signale": { "signale": {
+1 -4
View File
@@ -86,10 +86,7 @@ router
}, },
{ {
event_id: req.params.event_id, event_id: req.params.event_id,
expand: expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
}, },
); );
+7 -29
View File
@@ -44,18 +44,11 @@ router
}, },
}, },
{ {
expand: expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
query: typeof req.query.query === "string" ? req.query.query : null, query: typeof req.query.query === "string" ? req.query.query : null,
}, },
); );
const rows = await internalCertificate.getAll( const rows = await internalCertificate.getAll(res.locals.access, data.expand, data.query);
res.locals.access,
data.expand,
data.query,
);
res.status(200).send(rows); res.status(200).send(rows);
} catch (err) { } catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -70,15 +63,9 @@ router
*/ */
.post(async (req, res, next) => { .post(async (req, res, next) => {
try { try {
const payload = await apiValidator( const payload = await apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body);
getValidationSchema("/nginx/certificates", "post"),
req.body,
);
req.setTimeout(900000); // 15 minutes timeout req.setTimeout(900000); // 15 minutes timeout
const result = await internalCertificate.create( const result = await internalCertificate.create(res.locals.access, payload);
res.locals.access,
payload,
);
res.status(201).send(result); res.status(201).send(result);
} catch (err) { } catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -139,16 +126,10 @@ router
*/ */
.post(async (req, res, next) => { .post(async (req, res, next) => {
try { try {
const payload = await apiValidator( const payload = await apiValidator(getValidationSchema("/nginx/certificates/test-http", "post"), req.body);
getValidationSchema("/nginx/certificates/test-http", "post"),
req.body,
);
req.setTimeout(60000); // 1 minute timeout req.setTimeout(60000); // 1 minute timeout
const result = await internalCertificate.testHttpsChallenge( const result = await internalCertificate.testHttpsChallenge(res.locals.access, payload);
res.locals.access,
payload,
);
res.status(200).send(result); res.status(200).send(result);
} catch (err) { } catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -224,10 +205,7 @@ router
}, },
{ {
certificate_id: req.params.certificate_id, certificate_id: req.params.certificate_id,
expand: expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
}, },
); );
const row = await internalCertificate.get(res.locals.access, { const row = await internalCertificate.get(res.locals.access, {
+10 -41
View File
@@ -48,18 +48,11 @@ router
}, },
}, },
{ {
expand: expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
query: typeof req.query.query === "string" ? req.query.query : null, query: typeof req.query.query === "string" ? req.query.query : null,
}, },
); );
const users = await internalUser.getAll( const users = await internalUser.getAll(res.locals.access, data.expand, data.query);
res.locals.access,
data.expand,
data.query,
);
res.status(200).send(users); res.status(200).send(users);
} catch (err) { } catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -95,10 +88,7 @@ router
} }
} }
const payload = await apiValidator( const payload = await apiValidator(getValidationSchema("/users", "post"), body);
getValidationSchema("/users", "post"),
body,
);
const user = await internalUser.create(res.locals.access, payload); const user = await internalUser.create(res.locals.access, payload);
res.status(201).send(user); res.status(201).send(user);
} catch (err) { } catch (err) {
@@ -169,20 +159,14 @@ router
}, },
{ {
user_id: req.params.user_id, user_id: req.params.user_id,
expand: expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
}, },
); );
const user = await internalUser.get(res.locals.access, { const user = await internalUser.get(res.locals.access, {
id: data.user_id, id: data.user_id,
expand: data.expand, expand: data.expand,
omit: internalUser.getUserOmisionsByAccess( omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id),
res.locals.access,
data.user_id,
),
}); });
res.status(200).send(user); res.status(200).send(user);
} catch (err) { } catch (err) {
@@ -198,10 +182,7 @@ router
*/ */
.put(async (req, res, next) => { .put(async (req, res, next) => {
try { try {
const payload = await apiValidator( const payload = await apiValidator(getValidationSchema("/users/{userID}", "put"), req.body);
getValidationSchema("/users/{userID}", "put"),
req.body,
);
payload.id = req.params.user_id; payload.id = req.params.user_id;
const result = await internalUser.update(res.locals.access, payload); const result = await internalUser.update(res.locals.access, payload);
res.status(200).send(result); res.status(200).send(result);
@@ -248,10 +229,7 @@ router
*/ */
.put(async (req, res, next) => { .put(async (req, res, next) => {
try { try {
const payload = await apiValidator( const payload = await apiValidator(getValidationSchema("/users/{userID}/auth", "put"), req.body);
getValidationSchema("/users/{userID}/auth", "put"),
req.body,
);
payload.id = req.params.user_id; payload.id = req.params.user_id;
const result = await internalUser.setPassword(res.locals.access, payload); const result = await internalUser.setPassword(res.locals.access, payload);
res.status(200).send(result); res.status(200).send(result);
@@ -281,15 +259,9 @@ router
*/ */
.put(async (req, res, next) => { .put(async (req, res, next) => {
try { try {
const payload = await apiValidator( const payload = await apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body);
getValidationSchema("/users/{userID}/permissions", "put"),
req.body,
);
payload.id = req.params.user_id; payload.id = req.params.user_id;
const result = await internalUser.setPermissions( const result = await internalUser.setPermissions(res.locals.access, payload);
res.locals.access,
payload,
);
res.status(200).send(result); res.status(200).send(result);
} catch (err) { } catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -408,10 +380,7 @@ router
*/ */
.post(async (req, res, next) => { .post(async (req, res, next) => {
try { try {
const { code } = await apiValidator( const { code } = await apiValidator(getValidationSchema("/users/{userID}/2fa/enable", "post"), req.body);
getValidationSchema("/users/{userID}/2fa/enable", "post"),
req.body,
);
const result = await internal2FA.enable(res.locals.access, req.params.user_id, code); const result = await internal2FA.enable(res.locals.access, req.params.user_id, code);
res.status(200).send(result); res.status(200).send(result);
} catch (err) { } catch (err) {
+32 -35
View File
@@ -1,3 +1,4 @@
import fs from "node:fs/promises";
import { installPlugins } from "./lib/certbot.js"; import { installPlugins } from "./lib/certbot.js";
import utils from "./lib/utils.js"; import utils from "./lib/utils.js";
import { setup as logger } from "./logger.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 settingModel from "./models/setting.js";
import userModel from "./models/user.js"; import userModel from "./models/user.js";
import userPermissionModel from "./models/user_permission.js"; import userPermissionModel from "./models/user_permission.js";
import fs from "fs/promises";
export const isSetup = async () => { export const isSetup = async () => {
const row = await userModel.query().select("id").where("is_deleted", 0).first(); const row = await userModel.query().select("id").where("is_deleted", 0).first();
return row?.id > 0; return row?.id > 0;
} };
/** /**
* Creates a default admin users if one doesn't already exist in the database * Creates a default admin users if one doesn't already exist in the database
@@ -45,18 +45,14 @@ const setupDefaultUser = async () => {
roles: ["admin"], roles: ["admin"],
}; };
const user = await userModel const user = await userModel.query().insertAndFetch(data);
.query()
.insertAndFetch(data);
await authModel await authModel.query().insert({
.query() user_id: user.id,
.insert({ type: "password",
user_id: user.id, secret: initialAdminPassword,
type: "password", meta: {},
secret: initialAdminPassword, });
meta: {},
});
await userPermissionModel.query().insert({ await userPermissionModel.query().insert({
user_id: user.id, user_id: user.id,
@@ -78,22 +74,16 @@ const setupDefaultUser = async () => {
* @returns {Promise} * @returns {Promise}
*/ */
const setupDefaultSettings = async () => { const setupDefaultSettings = async () => {
const row = await settingModel const row = await settingModel.query().select("id").where({ id: "default-site" }).first();
.query()
.select("id")
.where({ id: "default-site" })
.first();
if (!row?.id) { if (!row?.id) {
await settingModel await settingModel.query().insert({
.query() id: "default-site",
.insert({ name: "Default Site",
id: "default-site", description: "What to show when Nginx is hit with an unknown Host",
name: "Default Site", value: "congratulations",
description: "What to show when Nginx is hit with an unknown Host", meta: {},
value: "congratulations", });
meta: {},
});
logger.info("Default settings added"); logger.info("Default settings added");
} }
}; };
@@ -104,10 +94,7 @@ const setupDefaultSettings = async () => {
* @returns {Promise} * @returns {Promise}
*/ */
const setupCertbotPlugins = async () => { const setupCertbotPlugins = async () => {
const certificates = await certificateModel const certificates = await certificateModel.query().where("is_deleted", 0).andWhere("provider", "letsencrypt");
.query()
.where("is_deleted", 0)
.andWhere("provider", "letsencrypt");
if (certificates?.length) { if (certificates?.length) {
const plugins = []; const plugins = [];
@@ -122,14 +109,24 @@ const setupCertbotPlugins = async () => {
// Make sure credentials file exists // Make sure credentials file exists
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
if (typeof certificate.meta.dns_provider_credentials === "string") { if (typeof certificate.meta.dns_provider_credentials === "string") {
promises.push(fs.mkdir("/etc/letsencrypt/credentials", { recursive: true }) promises.push(
.then(() => fs.writeFile(credentials_loc, certificate.meta.dns_provider_credentials, { mode: 0o600, flag: "wx" })) fs
.catch((err) => { if (err.code !== "EEXIST") throw err; })); .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; return true;
}); });
await installPlugins(plugins); await installPlugins(plugins);
if (promises.length) { if (promises.length) {
+386 -339
View File
File diff suppressed because it is too large Load Diff