Compare commits

..

4 Commits

Author SHA1 Message Date
Jamie Curnow
432afe73ad User table polishing, user delete modal 2025-09-04 14:59:01 +10:00
Jamie Curnow
5a01da2916 Notification toasts, nicer loading, add new user support 2025-09-04 12:11:39 +10:00
Jamie Curnow
ebd9148813 React 2025-09-03 14:02:14 +10:00
Jamie Curnow
a12553fec7 Convert backend to ESM
- About 5 years overdue
- Remove eslint, use bomejs instead
2025-09-03 13:59:40 +10:00
230 changed files with 4147 additions and 9546 deletions

View File

@@ -1 +1 @@
2.13.0 2.12.6

View File

@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<img src="https://nginxproxymanager.com/github.png"> <img src="https://nginxproxymanager.com/github.png">
<br><br> <br><br>
<img src="https://img.shields.io/badge/version-2.13.0-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.12.6-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager"> <a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge"> <img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a> </a>
@@ -88,6 +88,14 @@ Sometimes this can take a little bit because of the entropy of keys.
[http://127.0.0.1:81](http://127.0.0.1:81) [http://127.0.0.1:81](http://127.0.0.1:81)
Default Admin User:
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.
## Contributing ## Contributing

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",

View File

@@ -21,74 +21,88 @@ const internalAccessList = {
* @param {Object} data * @param {Object} data
* @returns {Promise} * @returns {Promise}
*/ */
create: async (access, data) => { create: (access, data) => {
await access.can("access_lists:create", data); return access
const row = await accessListModel .can("access_lists:create", data)
.query() .then((/*access_data*/) => {
.insertAndFetch({ return accessListModel
name: data.name, .query()
satisfy_any: data.satisfy_any, .insertAndFetch({
pass_auth: data.pass_auth, name: data.name,
owner_user_id: access.token.getUserId(1), satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1),
})
.then(utils.omitRow(omissions()));
}) })
.then(utils.omitRow(omissions())); .then((row) => {
data.id = row.id;
data.id = row.id; const promises = [];
const promises = []; // Now add the items
// Items data.items.map((item) => {
data.items.map((item) => { promises.push(
promises.push( accessListAuthModel.query().insert({
accessListAuthModel.query().insert({ access_list_id: row.id,
access_list_id: row.id, username: item.username,
username: item.username, password: item.password,
password: item.password, }),
}), );
); return true;
return true; });
});
// Clients // Now add the clients
data.clients?.map((client) => { if (typeof data.clients !== "undefined" && data.clients) {
promises.push( data.clients.map((client) => {
accessListClientModel.query().insert({ promises.push(
access_list_id: row.id, accessListClientModel.query().insert({
address: client.address, access_list_id: row.id,
directive: client.directive, address: client.address,
}), directive: client.directive,
); }),
return true; );
}); return true;
});
}
await Promise.all(promises); return Promise.all(promises);
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
},
true /* <- skip masking */,
);
})
.then((row) => {
// Audit log
data.meta = _.assign({}, data.meta || {}, row.meta);
// re-fetch with expansions return internalAccessList
const freshRow = await internalAccessList.get( .build(row)
access, .then(() => {
{ if (Number.parseInt(row.proxy_host_count, 10)) {
id: data.id, return internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"], }
}, })
true // skip masking .then(() => {
); // Add to audit log
return internalAuditLog.add(access, {
// Audit log action: "created",
data.meta = _.assign({}, data.meta || {}, freshRow.meta); object_type: "access-list",
await internalAccessList.build(freshRow); object_id: row.id,
meta: internalAccessList.maskItems(data),
if (Number.parseInt(freshRow.proxy_host_count, 10)) { });
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts); })
} .then(() => {
return internalAccessList.maskItems(row);
// Add to audit log });
await internalAuditLog.add(access, { });
action: "created",
object_type: "access-list",
object_id: freshRow.id,
meta: internalAccessList.maskItems(data),
});
return internalAccessList.maskItems(freshRow);
}, },
/** /**
@@ -99,107 +113,127 @@ const internalAccessList = {
* @param {String} [data.items] * @param {String} [data.items]
* @return {Promise} * @return {Promise}
*/ */
update: async (access, data) => { update: (access, data) => {
await access.can("access_lists:update", data.id); return access
const row = await internalAccessList.get(access, { id: data.id }); .can("access_lists:update", data.id)
if (row.id !== data.id) { .then((/*access_data*/) => {
// Sanity check that something crazy hasn't happened return internalAccessList.get(access, { id: data.id });
throw new errs.InternalValidationError( })
`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`, .then((row) => {
); if (row.id !== data.id) {
} // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
// patch name if specified `Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
if (typeof data.name !== "undefined" && data.name) {
await accessListModel.query().where({ id: data.id }).patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
// Check for items and add/update/remove them
if (typeof data.items !== "undefined" && data.items) {
const promises = [];
const itemsToKeep = [];
data.items.map((item) => {
if (item.password) {
promises.push(
accessListAuthModel.query().insert({
access_list_id: data.id,
username: item.username,
password: item.password,
}),
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
itemsToKeep.push(item.username);
}
return true;
});
const query = accessListAuthModel.query().delete().where("access_list_id", data.id);
if (itemsToKeep.length) {
query.andWhere("username", "NOT IN", itemsToKeep);
}
await query;
// Add new items
if (promises.length) {
await Promise.all(promises);
}
}
// Check for clients and add/update/remove them
if (typeof data.clients !== "undefined" && data.clients) {
const clientPromises = [];
data.clients.map((client) => {
if (client.address) {
clientPromises.push(
accessListClientModel.query().insert({
access_list_id: data.id,
address: client.address,
directive: client.directive,
}),
); );
} }
return true; })
.then(() => {
// patch name if specified
if (typeof data.name !== "undefined" && data.name) {
return accessListModel.query().where({ id: data.id }).patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
})
.then(() => {
// Check for items and add/update/remove them
if (typeof data.items !== "undefined" && data.items) {
const promises = [];
const items_to_keep = [];
data.items.map((item) => {
if (item.password) {
promises.push(
accessListAuthModel.query().insert({
access_list_id: data.id,
username: item.username,
password: item.password,
}),
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
items_to_keep.push(item.username);
}
return true;
});
const query = accessListAuthModel.query().delete().where("access_list_id", data.id);
if (items_to_keep.length) {
query.andWhere("username", "NOT IN", items_to_keep);
}
return query.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Check for clients and add/update/remove them
if (typeof data.clients !== "undefined" && data.clients) {
const promises = [];
data.clients.map((client) => {
if (client.address) {
promises.push(
accessListClientModel.query().insert({
access_list_id: data.id,
address: client.address,
directive: client.directive,
}),
);
}
return true;
});
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
return query.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "updated",
object_type: "access-list",
object_id: data.id,
meta: internalAccessList.maskItems(data),
});
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
},
true /* <- skip masking */,
);
})
.then((row) => {
return internalAccessList
.build(row)
.then(() => {
if (Number.parseInt(row.proxy_host_count, 10)) {
return internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}
})
.then(internalNginx.reload)
.then(() => {
return internalAccessList.maskItems(row);
});
}); });
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
await query;
// Add new clitens
if (clientPromises.length) {
await Promise.all(clientPromises);
}
}
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "access-list",
object_id: data.id,
meta: internalAccessList.maskItems(data),
});
// re-fetch with expansions
const freshRow = await internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
},
true // skip masking
);
await internalAccessList.build(freshRow)
if (Number.parseInt(row.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}
await internalNginx.reload();
return internalAccessList.maskItems(row);
}, },
/** /**
@@ -208,50 +242,55 @@ const internalAccessList = {
* @param {Integer} data.id * @param {Integer} data.id
* @param {Array} [data.expand] * @param {Array} [data.expand]
* @param {Array} [data.omit] * @param {Array} [data.omit]
* @param {Boolean} [skipMasking] * @param {Boolean} [skip_masking]
* @return {Promise} * @return {Promise}
*/ */
get: async (access, data, skipMasking) => { get: (access, data, skip_masking) => {
const thisData = data || {}; const thisData = data || {};
const accessData = await access.can("access_lists:get", thisData.id)
const query = accessListModel return access
.query() .can("access_lists:get", thisData.id)
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count")) .then((accessData) => {
.leftJoin("proxy_host", function () { const query = accessListModel
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn( .query()
"proxy_host.is_deleted", .select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
"=", .leftJoin("proxy_host", function () {
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)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]")
.first();
if (accessData.permission_visibility !== "all") {
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
}) })
.where("access_list.is_deleted", 0) .then((row) => {
.andWhere("access_list.id", thisData.id) let thisRow = row;
.groupBy("access_list.id") if (!row || !row.id) {
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]") throw new errs.ItemNotFoundError(thisData.id);
.first(); }
if (!skip_masking && typeof thisRow.items !== "undefined" && thisRow.items) {
if (accessData.permission_visibility !== "all") { thisRow = internalAccessList.maskItems(thisRow);
query.andWhere("access_list.owner_user_id", access.token.getUserId(1)); }
} // Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { thisRow = _.omit(thisRow, data.omit);
query.withGraphFetched(`[${thisData.expand.join(", ")}]`); }
} return thisRow;
});
let row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
if (!skipMasking && typeof row.items !== "undefined" && row.items) {
row = internalAccessList.maskItems(row);
}
// Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
row = _.omit(row, data.omit);
}
return row;
}, },
/** /**
@@ -261,64 +300,75 @@ const internalAccessList = {
* @param {String} [data.reason] * @param {String} [data.reason]
* @returns {Promise} * @returns {Promise}
*/ */
delete: async (access, data) => { delete: (access, data) => {
await access.can("access_lists:delete", data.id); return access
const row = await internalAccessList.get(access, { .can("access_lists:delete", data.id)
id: data.id, .then(() => {
expand: ["proxy_hosts", "items", "clients"], return internalAccessList.get(access, { id: data.id, expand: ["proxy_hosts", "items", "clients"] });
}); })
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row || !row.id) { // 1. update row to be deleted
throw new errs.ItemNotFoundError(data.id); // 2. update any proxy hosts that were using it (ignoring permissions)
} // 3. reconfigure those hosts
// 4. audit log
// 1. update row to be deleted // 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions) return accessListModel
// 3. reconfigure those hosts .query()
// 4. audit log .where("id", row.id)
.patch({
is_deleted: 1,
})
.then(() => {
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
return proxyHostModel
.query()
.where("access_list_id", "=", row.id)
.patch({ access_list_id: 0 })
.then(() => {
// 3. reconfigure those hosts, then reload nginx
// 1. update row to be deleted // set the access_list_id to zero for these items
await accessListModel row.proxy_hosts.map((_val, idx) => {
.query() row.proxy_hosts[idx].access_list_id = 0;
.where("id", row.id) return true;
.patch({ });
is_deleted: 1,
});
// 2. update any proxy hosts that were using it (ignoring permissions) return internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
if (row.proxy_hosts) { })
await proxyHostModel .then(() => {
.query() return internalNginx.reload();
.where("access_list_id", "=", row.id) });
.patch({ access_list_id: 0 }); }
})
.then(() => {
// delete the htpasswd file
const htpasswd_file = internalAccessList.getFilename(row);
// 3. reconfigure those hosts, then reload nginx try {
// set the access_list_id to zero for these items fs.unlinkSync(htpasswd_file);
row.proxy_hosts.map((_val, idx) => { } catch (_err) {
row.proxy_hosts[idx].access_list_id = 0; // do nothing
}
})
.then(() => {
// 4. audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "access-list",
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ["is_deleted", "proxy_hosts"]),
});
});
})
.then(() => {
return true; return true;
}); });
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}
await internalNginx.reload();
// delete the htpasswd file
try {
fs.unlinkSync(internalAccessList.getFilename(row));
} catch (_err) {
// do nothing
}
// 4. audit log
await internalAuditLog.add(access, {
action: "deleted",
object_type: "access-list",
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ["is_deleted", "proxy_hosts"]),
});
return true;
}, },
/** /**
@@ -326,73 +376,76 @@ const internalAccessList = {
* *
* @param {Access} access * @param {Access} access
* @param {Array} [expand] * @param {Array} [expand]
* @param {String} [searchQuery] * @param {String} [search_query]
* @returns {Promise} * @returns {Promise}
*/ */
getAll: async (access, expand, searchQuery) => { getAll: (access, expand, search_query) => {
const accessData = await access.can("access_lists:list"); return access
.can("access_lists:list")
.then((access_data) => {
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,
);
})
.where("access_list.is_deleted", 0)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients]")
.orderBy("access_list.name", "ASC");
const query = accessListModel if (access_data.permission_visibility !== "all") {
.query() query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
.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,
);
})
.where("access_list.is_deleted", 0)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients]")
.orderBy("access_list.name", "ASC");
if (accessData.permission_visibility !== "all") {
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof searchQuery === "string") {
query.where(function () {
this.where("name", "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (rows) {
rows.map((row, idx) => {
if (typeof row.items !== "undefined" && row.items) {
rows[idx] = internalAccessList.maskItems(row);
} }
return true;
// Query is used for searching
if (typeof search_query === "string") {
query.where(function () {
this.where("name", "like", `%${search_query}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (rows) {
rows.map((row, idx) => {
if (typeof row.items !== "undefined" && row.items) {
rows[idx] = internalAccessList.maskItems(row);
}
return true;
});
}
return rows;
}); });
}
return rows;
}, },
/** /**
* Count is used in reports * Report use
* *
* @param {Integer} userId * @param {Integer} user_id
* @param {String} visibility * @param {String} visibility
* @returns {Promise} * @returns {Promise}
*/ */
getCount: async (userId, visibility) => { getCount: (user_id, 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", user_id);
} }
const row = await query.first(); return query.first().then((row) => {
return Number.parseInt(row.count, 10); return Number.parseInt(row.count, 10);
});
}, },
/** /**
@@ -402,19 +455,20 @@ const internalAccessList = {
maskItems: (list) => { maskItems: (list) => {
if (list && typeof list.items !== "undefined") { if (list && typeof list.items !== "undefined") {
list.items.map((val, idx) => { list.items.map((val, idx) => {
let repeatFor = 8; let repeat_for = 8;
let firstChar = "*"; let first_char = "*";
if (typeof val.password !== "undefined" && val.password) { if (typeof val.password !== "undefined" && val.password) {
repeatFor = val.password.length - 1; repeat_for = val.password.length - 1;
firstChar = val.password.charAt(0); first_char = val.password.charAt(0);
} }
list.items[idx].hint = firstChar + "*".repeat(repeatFor); list.items[idx].hint = first_char + "*".repeat(repeat_for);
list.items[idx].password = ""; list.items[idx].password = "";
return true; return true;
}); });
} }
return list; return list;
}, },
@@ -434,55 +488,66 @@ const internalAccessList = {
* @param {Array} list.items * @param {Array} list.items
* @returns {Promise} * @returns {Promise}
*/ */
build: async (list) => { build: (list) => {
logger.info(`Building Access file #${list.id} for: ${list.name}`); logger.info(`Building Access file #${list.id} for: ${list.name}`);
const htpasswdFile = internalAccessList.getFilename(list); return new Promise((resolve, reject) => {
const htpasswd_file = internalAccessList.getFilename(list);
// 1. remove any existing access file // 1. remove any existing access file
try { try {
fs.unlinkSync(htpasswdFile); fs.unlinkSync(htpasswd_file);
} catch (_err) { } catch (_err) {
// do nothing // do nothing
} }
// 2. create empty access file // 2. create empty access file
fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'}); try {
fs.writeFileSync(htpasswd_file, "", { encoding: "utf8" });
resolve(htpasswd_file);
} catch (err) {
reject(err);
}
}).then((htpasswd_file) => {
// 3. generate password for each user
if (list.items.length) {
return new Promise((resolve, reject) => {
batchflow(list.items)
.sequential()
.each((_i, item, next) => {
if (typeof item.password !== "undefined" && item.password.length) {
logger.info(`Adding: ${item.username}`);
// 3. generate password for each user utils
if (list.items.length) { .execFile("openssl", ["passwd", "-apr1", item.password])
await new Promise((resolve, reject) => { .then((res) => {
batchflow(list.items).sequential() try {
.each((_i, item, next) => { fs.appendFileSync(htpasswd_file, `${item.username}:${res}\n`, {
if (item.password?.length) { encoding: "utf8",
logger.info(`Adding: ${item.username}`); });
} catch (err) {
utils.execFile('openssl', ['passwd', '-apr1', item.password]) reject(err);
.then((res) => { }
try { next();
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'}); })
} catch (err) { .catch((err) => {
reject(err); logger.error(err);
} next(err);
next(); });
}) }
.catch((err) => { })
logger.error(err); .error((err) => {
next(err); logger.error(err);
}); reject(err);
} })
}) .end((results) => {
.error((err) => { logger.success(`Built Access file #${list.id} for: ${list.name}`);
logger.error(err); resolve(results);
reject(err); });
}) });
.end((results) => { }
logger.success(`Built Access file #${list.id} for: ${list.name}`); });
resolve(results); },
}); };
});
}
}
}
export default internalAccessList; export default internalAccessList;

View File

@@ -9,60 +9,31 @@ const internalAuditLog = {
* *
* @param {Access} access * @param {Access} access
* @param {Array} [expand] * @param {Array} [expand]
* @param {String} [searchQuery] * @param {String} [search_query]
* @returns {Promise} * @returns {Promise}
*/ */
getAll: async (access, expand, searchQuery) => { getAll: (access, expand, search_query) => {
await access.can("auditlog:list"); return access.can("auditlog:list").then(() => {
const query = auditLogModel
.query()
.orderBy("created_on", "DESC")
.orderBy("id", "DESC")
.limit(100)
.allowGraph("[user]");
const query = auditLogModel // Query is used for searching
.query() if (typeof search_query === "string" && search_query.length > 0) {
.orderBy("created_on", "DESC") query.where(function () {
.orderBy("id", "DESC") this.where(castJsonIfNeed("meta"), "like", `%${search_query}`);
.limit(100) });
.allowGraph("[user]"); }
// Query is used for searching if (typeof expand !== "undefined" && expand !== null) {
if (typeof searchQuery === "string" && searchQuery.length > 0) { query.withGraphFetched(`[${expand.join(", ")}]`);
query.where(function () { }
this.where(castJsonIfNeed("meta"), "like", `%${searchQuery}`);
});
}
if (typeof expand !== "undefined" && expand !== null) { return query;
query.withGraphFetched(`[${expand.join(", ")}]`); });
}
return await query;
},
/**
* @param {Access} access
* @param {Object} [data]
* @param {Integer} [data.id] Defaults to the token user
* @param {Array} [data.expand]
* @return {Promise}
*/
get: async (access, data) => {
await access.can("auditlog:list");
const query = auditLogModel
.query()
.andWhere("id", data.id)
.allowGraph("[user]")
.first();
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
}
const row = await query;
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
return row;
}, },
/** /**
@@ -79,22 +50,27 @@ const internalAuditLog = {
* @param {Object} [data.meta] * @param {Object} [data.meta]
* @returns {Promise} * @returns {Promise}
*/ */
add: async (access, data) => { add: (access, data) => {
if (typeof data.user_id === "undefined" || !data.user_id) { return new Promise((resolve, reject) => {
data.user_id = access.token.getUserId(1); // Default the user id
} if (typeof data.user_id === "undefined" || !data.user_id) {
data.user_id = access.token.getUserId(1);
}
if (typeof data.action === "undefined" || !data.action) { if (typeof data.action === "undefined" || !data.action) {
throw new errs.InternalValidationError("Audit log entry must contain an Action"); reject(new errs.InternalValidationError("Audit log entry must contain an Action"));
} } else {
// Make sure at least 1 of the IDs are set and action
// Make sure at least 1 of the IDs are set and action resolve(
return await auditLogModel.query().insert({ auditLogModel.query().insert({
user_id: data.user_id, user_id: data.user_id,
action: data.action, action: data.action,
object_type: data.object_type || "", object_type: data.object_type || "",
object_id: data.object_id || 0, object_id: data.object_id || 0,
meta: data.meta || {}, meta: data.meta || {},
}),
);
}
}); });
}, },
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -18,79 +18,91 @@ const internalDeadHost = {
* @param {Object} data * @param {Object} data
* @returns {Promise} * @returns {Promise}
*/ */
create: async (access, data) => { create: (access, data) => {
const createCertificate = data.certificate_id === "new"; const createCertificate = data.certificate_id === "new";
if (createCertificate) { if (createCertificate) {
delete data.certificate_id; delete data.certificate_id;
} }
await access.can("dead_hosts:create", data); return access
.can("dead_hosts:create", data)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
// Get a list of the domain names and check each of them against existing records data.domain_names.map((domain_name) => {
const domainNameCheckPromises = []; domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
data.domain_names.map((domain_name) => { return Promise.all(domain_name_check_promises).then((check_results) => {
domainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name)); check_results.map((result) => {
return true; if (result.is_taken) {
}); throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
})
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
const thisData = internalHost.cleanSslHstsData(data);
await Promise.all(domainNameCheckPromises).then((check_results) => { // Fix for db field not having a default value
check_results.map((result) => { // for this optional field.
if (result.is_taken) { if (typeof data.advanced_config === "undefined") {
throw new errs.ValidationError(`${result.hostname} is already in use`); thisData.advanced_config = "";
} }
return true;
return deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalDeadHost.update(access, {
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalDeadHost.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(deadHostModel, "dead_host", row).then(() => {
return row;
});
})
.then((row) => {
data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "dead-host",
object_id: row.id,
meta: data,
})
.then(() => {
return row;
});
}); });
});
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
const thisData = internalHost.cleanSslHstsData(data);
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === "undefined") {
thisData.advanced_config = "";
}
const row = await deadHostModel.query()
.insertAndFetch(thisData)
.then(utils.omitRow(omissions()));
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "dead-host",
object_id: row.id,
meta: thisData,
});
if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, data);
// update host with cert id
await internalDeadHost.update(access, {
id: row.id,
certificate_id: cert.id,
});
}
// re-fetch with cert
const freshRow = await internalDeadHost.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
// Sanity check
if (createCertificate && !freshRow.certificate_id) {
throw new errs.InternalValidationError("The host was created but the Certificate creation failed.");
}
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", freshRow);
return freshRow;
}, },
/** /**
@@ -99,85 +111,107 @@ const internalDeadHost = {
* @param {Number} data.id * @param {Number} data.id
* @return {Promise} * @return {Promise}
*/ */
update: async (access, data) => { update: (access, data) => {
const createCertificate = data.certificate_id === "new"; let thisData = data;
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) { if (createCertificate) {
delete data.certificate_id; delete thisData.certificate_id;
} }
await access.can("dead_hosts:update", data.id); return access
.can("dead_hosts:update", thisData.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
// Get a list of the domain names and check each of them against existing records if (typeof thisData.domain_names !== "undefined") {
const domainNameCheckPromises = []; thisData.domain_names.map((domain_name) => {
if (typeof data.domain_names !== "undefined") { domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, "dead", data.id));
data.domain_names.map((domainName) => { return true;
domainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, "dead", data.id)); });
return true;
});
const checkResults = await Promise.all(domainNameCheckPromises); return Promise.all(domain_name_check_promises).then((check_results) => {
checkResults.map((result) => { check_results.map((result) => {
if (result.is_taken) { if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`); throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
} }
return true; })
.then(() => {
return internalDeadHost.get(access, { id: thisData.id });
})
.then((row) => {
if (row.id !== thisData.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`404 Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
}
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
.then((cert) => {
// update host with cert id
thisData.certificate_id = cert.id;
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
data,
);
thisData = internalHost.cleanSslHstsData(thisData, row);
return deadHostModel
.query()
.where({ id: thisData.id })
.patch(thisData)
.then((saved_row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "dead-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalDeadHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
})
.then((row) => {
// Configure nginx
return internalNginx.configure(deadHostModel, "dead_host", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
}); });
}
const row = await internalDeadHost.get(access, { id: data.id });
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`404 Host could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta),
});
// update host with cert id
data.certificate_id = cert.id;
}
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
let thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
data,
);
thisData = internalHost.cleanSslHstsData(thisData, row);
// do the row update
await deadHostModel
.query()
.where({id: data.id})
.patch(data);
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "dead-host",
object_id: row.id,
meta: thisData,
});
const thisRow = await internalDeadHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
});
// Configure nginx
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row);
row.meta = newMeta;
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
}, },
/** /**
@@ -188,32 +222,39 @@ const internalDeadHost = {
* @param {Array} [data.omit] * @param {Array} [data.omit]
* @return {Promise} * @return {Promise}
*/ */
get: async (access, data) => { get: (access, data) => {
const accessData = await access.can("dead_hosts:get", data.id); const thisData = data || {};
const query = deadHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", data.id)
.allowGraph("[owner,certificate]")
.first();
if (accessData.permission_visibility !== "all") { return access
query.andWhere("owner_user_id", access.token.getUserId(1)); .can("dead_hosts:get", thisData.id)
} .then((access_data) => {
const query = deadHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", dthisDataata.id)
.allowGraph("[owner,certificate]")
.first();
if (typeof data.expand !== "undefined" && data.expand !== null) { if (access_data.permission_visibility !== "all") {
query.withGraphFetched(`[${data.expand.join(", ")}]`); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const row = await query.then(utils.omitRow(omissions())); if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
if (!row || !row.id) { query.withGraphFetched(`[${data.expand.join(", ")}]`);
throw new errs.ItemNotFoundError(data.id); }
}
// Custom omissions return query.then(utils.omitRow(omissions()));
if (typeof data.omit !== "undefined" && data.omit !== null) { })
return _.omit(row, data.omit); .then((row) => {
} if (!row || !row.id) {
return row; throw new errs.ItemNotFoundError(thisData.id);
}
// Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(row, thisData.omit);
}
return row;
});
}, },
/** /**
@@ -223,32 +264,42 @@ const internalDeadHost = {
* @param {String} [data.reason] * @param {String} [data.reason]
* @returns {Promise} * @returns {Promise}
*/ */
delete: async (access, data) => { delete: (access, data) => {
await access.can("dead_hosts:delete", data.id) return access
const row = await internalDeadHost.get(access, { id: data.id }); .can("dead_hosts:delete", data.id)
if (!row || !row.id) { .then(() => {
throw new errs.ItemNotFoundError(data.id); return internalDeadHost.get(access, { id: data.id });
} })
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
await deadHostModel return deadHostModel
.query() .query()
.where("id", row.id) .where("id", row.id)
.patch({ .patch({
is_deleted: 1, is_deleted: 1,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("dead_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
}); });
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "deleted",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
}, },
/** /**
@@ -258,39 +309,48 @@ const internalDeadHost = {
* @param {String} [data.reason] * @param {String} [data.reason]
* @returns {Promise} * @returns {Promise}
*/ */
enable: async (access, data) => { enable: (access, data) => {
await access.can("dead_hosts:update", data.id) return access
const row = await internalDeadHost.get(access, { .can("dead_hosts:update", data.id)
id: data.id, .then(() => {
expand: ["certificate", "owner"], return internalDeadHost.get(access, {
}); id: data.id,
if (!row || !row.id) { expand: ["certificate", "owner"],
throw new errs.ItemNotFoundError(data.id); });
} })
if (row.enabled) { .then((row) => {
throw new errs.ValidationError("Host is already enabled"); if (!row || !row.id) {
} throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
row.enabled = 1; row.enabled = 1;
await deadHostModel return deadHostModel
.query() .query()
.where("id", row.id) .where("id", row.id)
.patch({ .patch({
enabled: 1, enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(deadHostModel, "dead_host", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
}); });
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", row);
// Add to audit log
await internalAuditLog.add(access, {
action: "enabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
}, },
/** /**
@@ -300,37 +360,47 @@ const internalDeadHost = {
* @param {String} [data.reason] * @param {String} [data.reason]
* @returns {Promise} * @returns {Promise}
*/ */
disable: async (access, data) => { disable: (access, data) => {
await access.can("dead_hosts:update", data.id) return access
const row = await internalDeadHost.get(access, { id: data.id }); .can("dead_hosts:update", data.id)
if (!row || !row.id) { .then(() => {
throw new errs.ItemNotFoundError(data.id); return internalDeadHost.get(access, { id: data.id });
} })
if (!row.enabled) { .then((row) => {
throw new errs.ValidationError("Host is already disabled"); if (!row || !row.id) {
} throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
row.enabled = 0; row.enabled = 0;
await deadHostModel return deadHostModel
.query() .query()
.where("id", row.id) .where("id", row.id)
.patch({ .patch({
enabled: 0, enabled: 0,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("dead_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "disabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
}); });
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "disabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
}, },
/** /**
@@ -338,38 +408,44 @@ const internalDeadHost = {
* *
* @param {Access} access * @param {Access} access
* @param {Array} [expand] * @param {Array} [expand]
* @param {String} [searchQuery] * @param {String} [search_query]
* @returns {Promise} * @returns {Promise}
*/ */
getAll: async (access, expand, searchQuery) => { getAll: (access, expand, search_query) => {
const accessData = await access.can("dead_hosts:list") return access
const query = deadHostModel .can("dead_hosts:list")
.query() .then((access_data) => {
.where("is_deleted", 0) const query = deadHostModel
.groupBy("id") .query()
.allowGraph("[owner,certificate]") .where("is_deleted", 0)
.orderBy(castJsonIfNeed("domain_names"), "ASC"); .groupBy("id")
.allowGraph("[owner,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") { if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) { if (typeof search_query === "string" && search_query.length > 0) {
query.where(function () { query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`); this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
}); });
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
}, },
/** /**
@@ -379,15 +455,16 @@ const internalDeadHost = {
* @param {String} visibility * @param {String} visibility
* @returns {Promise} * @returns {Promise}
*/ */
getCount: async (user_id, visibility) => { getCount: (user_id, visibility) => {
const query = deadHostModel.query().count("id as count").where("is_deleted", 0); const query = deadHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== "all") { if (visibility !== "all") {
query.andWhere("owner_user_id", user_id); query.andWhere("owner_user_id", user_id);
} }
const row = await query.first(); return query.first().then((row) => {
return Number.parseInt(row.count, 10); return Number.parseInt(row.count, 10);
});
}, },
}; };

View File

@@ -65,33 +65,50 @@ const internalHost = {
}, },
/** /**
* This returns all the host types with any domain listed in the provided domainNames array. * This returns all the host types with any domain listed in the provided domain_names array.
* This is used by the certificates to temporarily disable any host that is using the domain * This is used by the certificates to temporarily disable any host that is using the domain
* *
* @param {Array} domainNames * @param {Array} domain_names
* @returns {Promise} * @returns {Promise}
*/ */
getHostsWithDomains: async (domainNames) => { getHostsWithDomains: (domain_names) => {
const responseObject = { const promises = [
total_count: 0, proxyHostModel.query().where("is_deleted", 0),
dead_hosts: [], redirectionHostModel.query().where("is_deleted", 0),
proxy_hosts: [], deadHostModel.query().where("is_deleted", 0),
redirection_hosts: [], ];
};
const proxyRes = await proxyHostModel.query().where("is_deleted", 0); return Promise.all(promises).then((promises_results) => {
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames); const response_object = {
responseObject.total_count += responseObject.proxy_hosts.length; total_count: 0,
dead_hosts: [],
proxy_hosts: [],
redirection_hosts: [],
};
const redirRes = await redirectionHostModel.query().where("is_deleted", 0); if (promises_results[0]) {
responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames); // Proxy Hosts
responseObject.total_count += responseObject.redirection_hosts.length; response_object.proxy_hosts = internalHost._getHostsWithDomains(promises_results[0], domain_names);
response_object.total_count += response_object.proxy_hosts.length;
}
const deadRes = await deadHostModel.query().where("is_deleted", 0); if (promises_results[1]) {
responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames); // Redirection Hosts
responseObject.total_count += responseObject.dead_hosts.length; response_object.redirection_hosts = internalHost._getHostsWithDomains(
promises_results[1],
domain_names,
);
response_object.total_count += response_object.redirection_hosts.length;
}
return responseObject; if (promises_results[2]) {
// Dead Hosts
response_object.dead_hosts = internalHost._getHostsWithDomains(promises_results[2], domain_names);
response_object.total_count += response_object.dead_hosts.length;
}
return response_object;
});
}, },
/** /**

View File

@@ -301,11 +301,8 @@ const internalNginx = {
* @param {String} filename * @param {String} filename
*/ */
deleteFile: (filename) => { deleteFile: (filename) => {
if (!fs.existsSync(filename)) { logger.debug(`Deleting file: ${filename}`);
return;
}
try { try {
logger.debug(`Deleting file: ${filename}`);
fs.unlinkSync(filename); fs.unlinkSync(filename);
} catch (err) { } catch (err) {
logger.debug("Could not delete file:", JSON.stringify(err, null, 2)); logger.debug("Could not delete file:", JSON.stringify(err, null, 2));

View File

@@ -420,35 +420,41 @@ const internalProxyHost = {
* @param {String} [search_query] * @param {String} [search_query]
* @returns {Promise} * @returns {Promise}
*/ */
getAll: async (access, expand, searchQuery) => { getAll: (access, expand, search_query) => {
const accessData = await access.can("proxy_hosts:list"); return access
const query = proxyHostModel .can("proxy_hosts:list")
.query() .then((access_data) => {
.where("is_deleted", 0) const query = proxyHostModel
.groupBy("id") .query()
.allowGraph("[owner,access_list,certificate]") .where("is_deleted", 0)
.orderBy(castJsonIfNeed("domain_names"), "ASC"); .groupBy("id")
.allowGraph("[owner,access_list,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") { if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) { if (typeof search_query === "string" && search_query.length > 0) {
query.where(function () { query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`); this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
}); });
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
}, },
/** /**

View File

@@ -348,7 +348,7 @@ const internalStream = {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "disabled", action: "disabled",
object_type: "stream", object_type: "stream-host",
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions()),
}); });

View File

@@ -18,66 +18,67 @@ export default {
* @param {String} [issuer] * @param {String} [issuer]
* @returns {Promise} * @returns {Promise}
*/ */
getTokenFromEmail: async (data, issuer) => { getTokenFromEmail: (data, issuer) => {
const Token = TokenModel(); const Token = TokenModel();
data.scope = data.scope || "user"; data.scope = data.scope || "user";
data.expiry = data.expiry || "1d"; data.expiry = data.expiry || "1d";
const user = await userModel return userModel
.query() .query()
.where("email", data.identity.toLowerCase().trim()) .where("email", data.identity.toLowerCase().trim())
.andWhere("is_deleted", 0) .andWhere("is_deleted", 0)
.andWhere("is_disabled", 0) .andWhere("is_disabled", 0)
.first(); .first()
.then((user) => {
if (user) {
// Get auth
return authModel
.query()
.where("user_id", "=", user.id)
.where("type", "=", "password")
.first()
.then((auth) => {
if (auth) {
return auth.verifyPassword(data.secret).then((valid) => {
if (valid) {
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
}
if (!user) { // Create a moment of the expiry expression
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); const expiry = parseDatePeriod(data.expiry);
} if (expiry === null) {
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
}
const auth = await authModel return Token.create({
.query() iss: issuer || "api",
.where("user_id", "=", user.id) attrs: {
.where("type", "=", "password") id: user.id,
.first(); },
scope: [data.scope],
if (!auth) { expiresIn: data.expiry,
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); }).then((signed) => {
} return {
token: signed.token,
const valid = await auth.verifyPassword(data.secret); expires: expiry.toISOString(),
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) { });
// The scope requested doesn't exist as a role against the user, }
// you shall not pass. throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
throw new errs.AuthError(`Invalid scope: ${data.scope}`); });
} }
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
// Create a moment of the expiry expression });
const expiry = parseDatePeriod(data.expiry);
if (expiry === null) {
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
}
const signed = await Token.create({
iss: issuer || "api",
attrs: {
id: user.id,
},
scope: [data.scope],
expiresIn: data.expiry,
});
return {
token: signed.token,
expires: expiry.toISOString(),
};
}, },
/** /**
@@ -87,7 +88,7 @@ export default {
* @param {String} [data.scope] Only considered if existing token scope is admin * @param {String} [data.scope] Only considered if existing token scope is admin
* @returns {Promise} * @returns {Promise}
*/ */
getFreshToken: async (access, data) => { getFreshToken: (access, data) => {
const Token = TokenModel(); const Token = TokenModel();
const thisData = data || {}; const thisData = data || {};
@@ -114,17 +115,17 @@ export default {
} }
} }
const signed = await Token.create({ return Token.create({
iss: "api", iss: "api",
scope: scope, scope: scope,
attrs: token_attrs, attrs: token_attrs,
expiresIn: thisData.expiry, expiresIn: thisData.expiry,
}).then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString(),
};
}); });
return {
token: signed.token,
expires: expiry.toISOString(),
};
} }
throw new error.AssertionFailedError("Existing token contained invalid user data"); throw new error.AssertionFailedError("Existing token contained invalid user data");
}, },
@@ -135,7 +136,7 @@ export default {
*/ */
getTokenFromUser: async (user) => { getTokenFromUser: async (user) => {
const expire = "1d"; const expire = "1d";
const Token = TokenModel(); const Token = new TokenModel();
const expiry = parseDatePeriod(expire); const expiry = parseDatePeriod(expire);
const signed = await Token.create({ const signed = await Token.create({

View File

@@ -9,21 +9,18 @@ import internalAuditLog from "./audit-log.js";
import internalToken from "./token.js"; import internalToken from "./token.js";
const omissions = () => { const omissions = () => {
return ["is_deleted", "permissions.id", "permissions.user_id", "permissions.created_on", "permissions.modified_on"]; return ["is_deleted"];
}; }
const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm" }); const DEFAULT_AVATAR = 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=200&d=mp&r=g';
const internalUser = { const internalUser = {
/** /**
* Create a user can happen unauthenticated only once and only when no active users exist.
* Otherwise, a valid auth method is required.
*
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @returns {Promise} * @returns {Promise}
*/ */
create: async (access, data) => { create: (access, data) => {
const auth = data.auth || null; const auth = data.auth || null;
delete data.auth; delete data.auth;
@@ -34,43 +31,61 @@ const internalUser = {
data.is_disabled = data.is_disabled ? 1 : 0; data.is_disabled = data.is_disabled ? 1 : 0;
} }
await access.can("users:create", data); return access
data.avatar = gravatar.url(data.email, { default: "mm" }); .can("users:create", data)
.then(() => {
data.avatar = gravatar.url(data.email, { default: "mm" });
return userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
})
.then((user) => {
if (auth) {
return authModel
.query()
.insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
meta: {},
})
.then(() => {
return user;
});
}
return user;
})
.then((user) => {
// Create permissions row as well
const is_admin = data.roles.indexOf("admin") !== -1;
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); return userPermissionModel
if (auth) { .query()
user = await authModel.query().insert({ .insert({
user_id: user.id, user_id: user.id,
type: auth.type, visibility: is_admin ? "all" : "user",
secret: auth.secret, proxy_hosts: "manage",
meta: {}, redirection_hosts: "manage",
dead_hosts: "manage",
streams: "manage",
access_lists: "manage",
certificates: "manage",
})
.then(() => {
return internalUser.get(access, { id: user.id, expand: ["permissions"] });
});
})
.then((user) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "user",
object_id: user.id,
meta: user,
})
.then(() => {
return user;
});
}); });
}
// Create permissions row as well
const isAdmin = data.roles.indexOf("admin") !== -1;
await userPermissionModel.query().insert({
user_id: user.id,
visibility: isAdmin ? "all" : "user",
proxy_hosts: "manage",
redirection_hosts: "manage",
dead_hosts: "manage",
streams: "manage",
access_lists: "manage",
certificates: "manage",
});
user = await internalUser.get(access, { id: user.id, expand: ["permissions"] });
await internalAuditLog.add(access, {
action: "created",
object_type: "user",
object_id: user.id,
meta: user,
});
return user;
}, },
/** /**
@@ -131,7 +146,7 @@ const internalUser = {
action: "updated", action: "updated",
object_type: "user", object_type: "user",
object_id: user.id, object_id: user.id,
meta: { ...data, id: user.id, name: user.name }, meta: data,
}) })
.then(() => { .then(() => {
return user; return user;
@@ -250,14 +265,6 @@ const internalUser = {
}); });
}, },
deleteAll: async () => {
await userModel
.query()
.patch({
is_deleted: 1,
});
},
/** /**
* This will only count the users * This will only count the users
* *
@@ -309,7 +316,11 @@ const internalUser = {
// Query is used for searching // Query is used for searching
if (typeof search_query === "string") { if (typeof search_query === "string") {
query.where(function () { query.where(function () {
this.where("name", "like", `%${search_query}%`).orWhere("email", "like", `%${search_query}%`); this.where("name", "like", `%${search_query}%`).orWhere(
"email",
"like",
`%${search_query}%`,
);
}); });
} }

View File

@@ -22,13 +22,13 @@ import errs from "./error.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
export default function (tokenString) { export default function (token_string) {
const Token = TokenModel(); const Token = TokenModel();
let tokenData = null; let token_data = null;
let initialised = false; let initialised = false;
const objectCache = {}; const object_cache = {};
let allowInternalAccess = false; let allow_internal_access = false;
let userRoles = []; let user_roles = [];
let permissions = {}; let permissions = {};
/** /**
@@ -36,58 +36,65 @@ export default function (tokenString) {
* *
* @returns {Promise} * @returns {Promise}
*/ */
this.init = async () => { this.init = () => {
if (initialised) { return new Promise((resolve, reject) => {
return; if (initialised) {
} resolve();
} else if (!token_string) {
if (!tokenString) { reject(new errs.PermissionError("Permission Denied"));
throw new errs.PermissionError("Permission Denied");
}
tokenData = await Token.load(tokenString);
// At this point we need to load the user from the DB and make sure they:
// - exist (and not soft deleted)
// - still have the appropriate scopes for this token
// This is only required when the User ID is supplied or if the token scope has `user`
if (
tokenData.attrs.id ||
(typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1)
) {
// Has token user id or token user scope
const user = await userModel
.query()
.where("id", tokenData.attrs.id)
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.allowGraph("[permissions]")
.withGraphFetched("[permissions]")
.first();
if (user) {
// make sure user has all scopes of the token
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
user.roles.push("user");
let ok = true;
_.forEach(tokenData.scope, (scope_item) => {
if (_.indexOf(user.roles, scope_item) === -1) {
ok = false;
}
});
if (!ok) {
throw new errs.AuthError("Invalid token scope for User");
}
initialised = true;
userRoles = user.roles;
permissions = user.permissions;
} else { } else {
throw new errs.AuthError("User cannot be loaded for Token"); resolve(
Token.load(token_string).then((data) => {
token_data = data;
// At this point we need to load the user from the DB and make sure they:
// - exist (and not soft deleted)
// - still have the appropriate scopes for this token
// This is only required when the User ID is supplied or if the token scope has `user`
if (
token_data.attrs.id ||
(typeof token_data.scope !== "undefined" &&
_.indexOf(token_data.scope, "user") !== -1)
) {
// Has token user id or token user scope
return userModel
.query()
.where("id", token_data.attrs.id)
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.allowGraph("[permissions]")
.withGraphFetched("[permissions]")
.first()
.then((user) => {
if (user) {
// make sure user has all scopes of the token
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
user.roles.push("user");
let is_ok = true;
_.forEach(token_data.scope, (scope_item) => {
if (_.indexOf(user.roles, scope_item) === -1) {
is_ok = false;
}
});
if (!is_ok) {
throw new errs.AuthError("Invalid token scope for User");
}
initialised = true;
user_roles = user.roles;
permissions = user.permissions;
} else {
throw new errs.AuthError("User cannot be loaded for Token");
}
});
}
initialised = true;
}),
);
} }
} });
initialised = true;
}; };
/** /**
@@ -95,66 +102,82 @@ export default function (tokenString) {
* This only applies to USER token scopes, as all other tokens are not really bound * This only applies to USER token scopes, as all other tokens are not really bound
* by object scopes * by object scopes
* *
* @param {String} objectType * @param {String} object_type
* @returns {Promise} * @returns {Promise}
*/ */
this.loadObjects = async (objectType) => { this.loadObjects = (object_type) => {
let objects = null; return new Promise((resolve, reject) => {
if (Token.hasScope("user")) {
if (
typeof token_data.attrs.id === "undefined" ||
!token_data.attrs.id
) {
reject(new errs.AuthError("User Token supplied without a User ID"));
} else {
const token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
let query;
if (Token.hasScope("user")) { if (typeof object_cache[object_type] === "undefined") {
if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) { switch (object_type) {
throw new errs.AuthError("User Token supplied without a User ID"); // USERS - should only return yourself
} case "users":
resolve(token_user_id ? [token_user_id] : []);
break;
const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0; // Proxy Hosts
case "proxy_hosts":
query = proxyHostModel
.query()
.select("id")
.andWhere("is_deleted", 0);
if (typeof objectCache[objectType] !== "undefined") { if (permissions.visibility === "user") {
objects = objectCache[objectType]; query.andWhere("owner_user_id", token_user_id);
} else { }
switch (objectType) {
// USERS - should only return yourself
case "users":
objects = tokenUserId ? [tokenUserId] : [];
break;
// Proxy Hosts resolve(
case "proxy_hosts": { query.then((rows) => {
const query = proxyHostModel const result = [];
.query() _.forEach(rows, (rule_row) => {
.select("id") result.push(rule_row.id);
.andWhere("is_deleted", 0); });
if (permissions.visibility === "user") { // enum should not have less than 1 item
query.andWhere("owner_user_id", tokenUserId); if (!result.length) {
result.push(0);
}
return result;
}),
);
break;
// DEFAULT: null
default:
resolve(null);
break;
} }
} else {
const rows = await query; resolve(object_cache[object_type]);
objects = [];
_.forEach(rows, (ruleRow) => {
objects.push(ruleRow.id);
});
// enum should not have less than 1 item
if (!objects.length) {
objects.push(0);
}
break;
} }
} }
objectCache[objectType] = objects; } else {
resolve(null);
} }
} }).then((objects) => {
return objects; object_cache[object_type] = objects;
return objects;
});
}; };
/** /**
* Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema
* *
* @param {String} permissionLabel * @param {String} permission_label
* @returns {Object} * @returns {Object}
*/ */
this.getObjectSchema = async (permissionLabel) => { this.getObjectSchema = (permission_label) => {
const baseObjectType = permissionLabel.split(":").shift(); const base_object_type = permission_label.split(":").shift();
const schema = { const schema = {
$id: "objects", $id: "objects",
@@ -177,39 +200,41 @@ export default function (tokenString) {
}, },
}; };
const result = await this.loadObjects(baseObjectType); return this.loadObjects(base_object_type).then((object_result) => {
if (typeof result === "object" && result !== null) { if (typeof object_result === "object" && object_result !== null) {
schema.properties[baseObjectType] = { schema.properties[base_object_type] = {
type: "number", type: "number",
enum: result, enum: object_result,
minimum: 1, minimum: 1,
}; };
} else { } else {
schema.properties[baseObjectType] = { schema.properties[base_object_type] = {
type: "number", type: "number",
minimum: 1, minimum: 1,
}; };
} }
return schema; return schema;
});
}; };
// here:
return { return {
token: Token, token: Token,
/** /**
* *
* @param {Boolean} [allowInternal] * @param {Boolean} [allow_internal]
* @returns {Promise} * @returns {Promise}
*/ */
load: async (allowInternal) => { load: (allow_internal) => {
if (tokenString) { return new Promise((resolve /*, reject*/) => {
return await Token.load(tokenString); if (token_string) {
} resolve(Token.load(token_string));
allowInternalAccess = allowInternal; } else {
return allowInternal || null; allow_internal_access = allow_internal;
resolve(allow_internal_access || null);
}
});
}, },
reloadObjects: this.loadObjects, reloadObjects: this.loadObjects,
@@ -221,7 +246,7 @@ export default function (tokenString) {
* @returns {Promise} * @returns {Promise}
*/ */
can: async (permission, data) => { can: async (permission, data) => {
if (allowInternalAccess === true) { if (allow_internal_access === true) {
return true; return true;
} }
@@ -233,7 +258,7 @@ export default function (tokenString) {
[permission]: { [permission]: {
data: data, data: data,
scope: Token.get("scope"), scope: Token.get("scope"),
roles: userRoles, roles: user_roles,
permission_visibility: permissions.visibility, permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts, permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts, permission_redirection_hosts: permissions.redirection_hosts,
@@ -252,9 +277,10 @@ export default function (tokenString) {
properties: {}, properties: {},
}; };
const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, { const rawData = fs.readFileSync(
encoding: "utf8", `${__dirname}/access/${permission.replace(/:/gim, "-")}.json`,
}); { encoding: "utf8" },
);
permissionSchema.properties[permission] = JSON.parse(rawData); permissionSchema.properties[permission] = JSON.parse(rawData);
const ajv = new Ajv({ const ajv = new Ajv({

View File

@@ -6,6 +6,46 @@ import utils from "./utils.js";
const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')"; const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')";
/**
* @param {array} pluginKeys
*/
const installPlugins = async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
batchflow(pluginKeys)
.sequential()
.each((_i, pluginKey, next) => {
certbot
.installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
);
} else {
resolve();
}
});
});
};
/** /**
* Installs a cerbot plugin given the key for the object from * Installs a cerbot plugin given the key for the object from
* ../global/certbot-dns-plugins.json * ../global/certbot-dns-plugins.json
@@ -44,43 +84,4 @@ const installPlugin = async (pluginKey) => {
}); });
}; };
/**
* @param {array} pluginKeys
*/
const installPlugins = async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
batchflow(pluginKeys)
.sequential()
.each((_i, pluginKey, next) => {
installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
);
} else {
resolve();
}
});
});
};
export { installPlugins, installPlugin }; export { installPlugins, installPlugin };

View File

@@ -199,13 +199,6 @@ const isPostgres = () => {
*/ */
const isDebugMode = () => !!process.env.DEBUG; const isDebugMode = () => !!process.env.DEBUG;
/**
* Are we running in CI?
*
* @returns {boolean}
*/
const isCI = () => process.env.CI === 'true' && process.env.DEBUG === 'true';
/** /**
* Returns a public key * Returns a public key
* *
@@ -241,4 +234,4 @@ const useLetsencryptServer = () => {
return null; return null;
}; };
export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer }; export { configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer };

View File

@@ -14,10 +14,7 @@ const errs = {
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name; this.name = this.constructor.name;
this.previous = previous; this.previous = previous;
this.message = "Not Found"; this.message = `Item Not Found - ${id}`;
if (id) {
this.message = `Not Found - ${id}`;
}
this.public = true; this.public = true;
this.status = 404; this.status = 404;
}, },

View File

@@ -1,15 +1,15 @@
import Access from "../access.js"; import Access from "../access.js";
export default () => { export default () => {
return async (_, res, next) => { return (_, res, next) => {
try { res.locals.access = null;
res.locals.access = null; const access = new Access(res.locals.token || null);
const access = new Access(res.locals.token || null); access
await access.load(); .load()
res.locals.access = access; .then(() => {
next(); res.locals.access = access;
} catch (err) { next();
next(err); })
} .catch(next);
}; };
}; };

View File

@@ -14,27 +14,30 @@ const ajv = new Ajv({
* @param {Object} payload * @param {Object} payload
* @returns {Promise} * @returns {Promise}
*/ */
const apiValidator = async (schema, payload /*, description*/) => { function apiValidator(schema, payload /*, description*/) {
if (!schema) { return new Promise(function Promise_apiValidator(resolve, reject) {
throw new errs.ValidationError("Schema is undefined"); if (schema === null) {
} reject(new errs.ValidationError("Schema is undefined"));
return;
}
// Can't use falsy check here as valid payload could be `0` or `false` if (typeof payload === "undefined") {
if (typeof payload === "undefined") { reject(new errs.ValidationError("Payload is undefined"));
throw new errs.ValidationError("Payload is undefined"); return;
} }
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; resolve(payload);
} } else {
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 = [validate.errors, payload];
err.debug = [validate.errors, payload]; reject(err);
throw err; }
}; });
}
export default apiValidator; export default apiValidator;

View File

@@ -128,7 +128,7 @@ export default () => {
*/ */
getUserId: (defaultValue) => { getUserId: (defaultValue) => {
const attrs = self.get("attrs"); const attrs = self.get("attrs");
if (attrs?.id) { if (attrs && typeof attrs.id !== "undefined" && attrs.id) {
return attrs.id; return attrs.id;
} }

View File

@@ -3,5 +3,5 @@
"ignore": [ "ignore": [
"data" "data"
], ],
"ext": "js json ejs cjs" "ext": "js json ejs"
} }

View File

@@ -38,7 +38,7 @@
}, },
"devDependencies": { "devDependencies": {
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@biomejs/biome": "^2.2.4", "@biomejs/biome": "2.2.0",
"chalk": "4.1.2", "chalk": "4.1.2",
"nodemon": "^2.0.2" "nodemon": "^2.0.2"
}, },

View File

@@ -2,7 +2,6 @@ import express from "express";
import internalAuditLog from "../internal/audit-log.js"; import internalAuditLog from "../internal/audit-log.js";
import jwtdecode from "../lib/express/jwt-decode.js"; import jwtdecode from "../lib/express/jwt-decode.js";
import validator from "../lib/validator/index.js"; import validator from "../lib/validator/index.js";
import { express as logger } from "../logger.js";
const router = express.Router({ const router = express.Router({
caseSensitive: true, caseSensitive: true,
@@ -25,83 +24,31 @@ router
* *
* Retrieve all logs * Retrieve all logs
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, {
query: typeof req.query.query === "string" ? req.query.query : 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 internalAuditLog.getAll(res.locals.access, data.expand, data.query); )
res.status(200).send(rows); .then((data) => {
} catch (err) { return internalAuditLog.getAll(res.locals.access, data.expand, data.query);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((rows) => {
} res.status(200).send(rows);
}); })
.catch(next);
/**
* Specific audit log entry
*
* /api/audit-log/123
*/
router
.route("/:event_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log/123
*
* Retrieve a specific entry
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["event_id"],
additionalProperties: false,
properties: {
event_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
event_id: req.params.event_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
},
);
const item = await internalAuditLog.get(res.locals.access, {
id: data.event_id,
expand: data.expand,
});
res.status(200).send(item);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
export default router; export default router;

View File

@@ -1,7 +1,6 @@
import express from "express"; import express from "express";
import errs from "../lib/error.js"; import errs from "../lib/error.js";
import pjson from "../package.json" with { type: "json" }; import pjson from "../package.json" with { type: "json" };
import { isSetup } from "../setup.js";
import auditLogRoutes from "./audit-log.js"; import auditLogRoutes from "./audit-log.js";
import accessListsRoutes from "./nginx/access_lists.js"; import accessListsRoutes from "./nginx/access_lists.js";
import certificatesHostsRoutes from "./nginx/certificates.js"; import certificatesHostsRoutes from "./nginx/certificates.js";
@@ -25,13 +24,11 @@ const router = express.Router({
* Health Check * Health Check
* GET /api * GET /api
*/ */
router.get("/", async (_, res /*, next*/) => { router.get("/", (_, res /*, next*/) => {
const version = pjson.version.split("-").shift().split("."); const version = pjson.version.split("-").shift().split(".");
const setup = await isSetup();
res.status(200).send({ res.status(200).send({
status: "OK", status: "OK",
setup,
version: { version: {
major: Number.parseInt(version.shift(), 10), major: Number.parseInt(version.shift(), 10),
minor: Number.parseInt(version.shift(), 10), minor: Number.parseInt(version.shift(), 10),

View File

@@ -3,7 +3,6 @@ import internalAccessList from "../../internal/access-list.js";
import jwtdecode from "../../lib/express/jwt-decode.js"; import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js"; import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js"; import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js"; import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -27,31 +26,31 @@ router
* *
* Retrieve all access-lists * Retrieve all access-lists
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, {
query: typeof req.query.query === "string" ? req.query.query : 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 internalAccessList.getAll(res.locals.access, data.expand, data.query); )
res.status(200).send(rows); .then((data) => {
} catch (err) { return internalAccessList.getAll(res.locals.access, data.expand, data.query);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((rows) => {
} res.status(200).send(rows);
})
.catch(next);
}) })
/** /**
@@ -59,15 +58,15 @@ router
* *
* Create a new access-list * Create a new access-list
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body); .then((payload) => {
const result = await internalAccessList.create(res.locals.access, payload); return internalAccessList.create(res.locals.access, payload);
res.status(201).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(201).send(result);
next(err); })
} .catch(next);
}); });
/** /**
@@ -87,35 +86,35 @@ router
* *
* Retrieve a specific access-list * Retrieve a specific access-list
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["list_id"],
required: ["list_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { list_id: {
list_id: { $ref: "common#/properties/id",
$ref: "common#/properties/id", },
}, expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand",
},
}, },
}, },
{ },
list_id: req.params.list_id, {
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, list_id: req.params.list_id,
}, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
); },
const row = await internalAccessList.get(res.locals.access, { )
id: Number.parseInt(data.list_id, 10), .then((data) => {
expand: data.expand, return internalAccessList.get(res.locals.access, {
}); id: Number.parseInt(data.list_id, 10),
res.status(200).send(row); expand: data.expand,
} catch (err) { });
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((row) => {
} res.status(200).send(row);
})
.catch(next);
}) })
/** /**
@@ -123,16 +122,16 @@ router
* *
* Update and existing access-list * Update and existing access-list
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body); .then((payload) => {
payload.id = Number.parseInt(req.params.list_id, 10); payload.id = Number.parseInt(req.params.list_id, 10);
const result = await internalAccessList.update(res.locals.access, payload); return internalAccessList.update(res.locals.access, payload);
res.status(200).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(200).send(result);
next(err); })
} .catch(next);
}) })
/** /**
@@ -140,16 +139,13 @@ router
* *
* Delete and existing access-list * Delete and existing access-list
*/ */
.delete(async (req, res, next) => { .delete((req, res, next) => {
try { internalAccessList
const result = await internalAccessList.delete(res.locals.access, { .delete(res.locals.access, { id: Number.parseInt(req.params.list_id, 10) })
id: Number.parseInt(req.params.list_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
export default router; export default router;

View File

@@ -1,11 +1,9 @@
import express from "express"; import express from "express";
import dnsPlugins from "../../global/certbot-dns-plugins.json" with { type: "json" };
import internalCertificate from "../../internal/certificate.js"; import internalCertificate from "../../internal/certificate.js";
import errs from "../../lib/error.js"; import errs from "../../lib/error.js";
import jwtdecode from "../../lib/express/jwt-decode.js"; import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js"; import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js"; import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js"; import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -29,31 +27,31 @@ router
* *
* Retrieve all certificates * Retrieve all certificates
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, {
query: typeof req.query.query === "string" ? req.query.query : 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); )
res.status(200).send(rows); .then((data) => {
} catch (err) { return internalCertificate.getAll(res.locals.access, data.expand, data.query);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((rows) => {
} res.status(200).send(rows);
})
.catch(next);
}) })
/** /**
@@ -61,50 +59,16 @@ router
* *
* Create a new certificate * Create a new certificate
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body); .then((payload) => {
req.setTimeout(900000); // 15 minutes timeout req.setTimeout(900000); // 15 minutes timeout
const result = await internalCertificate.create(res.locals.access, payload); return internalCertificate.create(res.locals.access, payload);
res.status(201).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(201).send(result);
next(err); })
} .catch(next);
});
/**
* /api/nginx/certificates/dns-providers
*/
router
.route("/dns-providers")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/dns-providers
*
* Get list of all supported DNS providers
*/
.get(async (req, res, next) => {
try {
if (!res.locals.access.token.getUserId()) {
throw new errs.PermissionError("Login required");
}
const clean = Object.keys(dnsPlugins).map((key) => ({
id: key,
name: dnsPlugins[key].name,
credentials: dnsPlugins[key].credentials,
}));
clean.sort((a, b) => a.name.localeCompare(b.name));
res.status(200).send(clean);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -124,57 +88,18 @@ router
* *
* Test HTTP challenge for domains * Test HTTP challenge for domains
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
if (req.query.domains === undefined) { if (req.query.domains === undefined) {
next(new errs.ValidationError("Domains are required as query parameters")); next(new errs.ValidationError("Domains are required as query parameters"));
return; return;
} }
try { internalCertificate
const result = await internalCertificate.testHttpsChallenge( .testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains))
res.locals.access, .then((result) => {
JSON.parse(req.query.domains), res.status(200).send(result);
); })
res.status(200).send(result); .catch(next);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Validate Certs before saving
*
* /api/nginx/certificates/validate
*/
router
.route("/validate")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/validate
*
* Validate certificates
*/
.post(async (req, res, next) => {
if (!req.files) {
res.status(400).send({ error: "No files were uploaded" });
return;
}
try {
const result = await internalCertificate.validate({
files: req.files,
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -194,35 +119,35 @@ router
* *
* Retrieve a specific certificate * Retrieve a specific certificate
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["certificate_id"],
required: ["certificate_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { certificate_id: {
certificate_id: { $ref: "common#/properties/id",
$ref: "common#/properties/id", },
}, expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand",
},
}, },
}, },
{ },
certificate_id: req.params.certificate_id, {
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, certificate_id: req.params.certificate_id,
}, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
); },
const row = await internalCertificate.get(res.locals.access, { )
id: Number.parseInt(data.certificate_id, 10), .then((data) => {
expand: data.expand, return internalCertificate.get(res.locals.access, {
}); id: Number.parseInt(data.certificate_id, 10),
res.status(200).send(row); expand: data.expand,
} catch (err) { });
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((row) => {
} res.status(200).send(row);
})
.catch(next);
}) })
/** /**
@@ -230,16 +155,13 @@ router
* *
* Update and existing certificate * Update and existing certificate
*/ */
.delete(async (req, res, next) => { .delete((req, res, next) => {
try { internalCertificate
const result = await internalCertificate.delete(res.locals.access, { .delete(res.locals.access, { id: Number.parseInt(req.params.certificate_id, 10) })
id: Number.parseInt(req.params.certificate_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -259,21 +181,19 @@ router
* *
* Upload certificates * Upload certificates
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
if (!req.files) { if (!req.files) {
res.status(400).send({ error: "No files were uploaded" }); res.status(400).send({ error: "No files were uploaded" });
return; } else {
} internalCertificate
.upload(res.locals.access, {
try { id: Number.parseInt(req.params.certificate_id, 10),
const result = await internalCertificate.upload(res.locals.access, { files: req.files,
id: Number.parseInt(req.params.certificate_id, 10), })
files: req.files, .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
} }
}); });
@@ -294,17 +214,16 @@ router
* *
* Renew certificate * Renew certificate
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout req.setTimeout(900000); // 15 minutes timeout
try { internalCertificate
const result = await internalCertificate.renew(res.locals.access, { .renew(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10), id: Number.parseInt(req.params.certificate_id, 10),
}); })
res.status(200).send(result); .then((result) => {
} catch (err) { res.status(200).send(result);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .catch(next);
}
}); });
/** /**
@@ -324,15 +243,46 @@ router
* *
* Renew certificate * Renew certificate
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { internalCertificate
const result = await internalCertificate.download(res.locals.access, { .download(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10), id: Number.parseInt(req.params.certificate_id, 10),
}); })
res.status(200).download(result.fileName); .then((result) => {
} catch (err) { res.status(200).download(result.fileName);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .catch(next);
});
/**
* Validate Certs before saving
*
* /api/nginx/certificates/validate
*/
router
.route("/validate")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/validate
*
* Validate certificates
*/
.post((req, res, next) => {
if (!req.files) {
res.status(400).send({ error: "No files were uploaded" });
} else {
internalCertificate
.validate({
files: req.files,
})
.then((result) => {
res.status(200).send(result);
})
.catch(next);
} }
}); });

View File

@@ -3,7 +3,6 @@ import internalDeadHost from "../../internal/dead-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js"; import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js"; import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js"; import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js"; import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -27,31 +26,31 @@ router
* *
* Retrieve all dead-hosts * Retrieve all dead-hosts
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, {
query: typeof req.query.query === "string" ? req.query.query : 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 internalDeadHost.getAll(res.locals.access, data.expand, data.query); )
res.status(200).send(rows); .then((data) => {
} catch (err) { return internalDeadHost.getAll(res.locals.access, data.expand, data.query);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((rows) => {
} res.status(200).send(rows);
})
.catch(next);
}) })
/** /**
@@ -59,15 +58,15 @@ router
* *
* Create a new dead-host * Create a new dead-host
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/dead-hosts", "post"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts", "post"), req.body); .then((payload) => {
const result = await internalDeadHost.create(res.locals.access, payload); return internalDeadHost.create(res.locals.access, payload);
res.status(201).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(201).send(result);
next(err); })
} .catch(next);
}); });
/** /**
@@ -87,69 +86,66 @@ router
* *
* Retrieve a specific dead-host * Retrieve a specific dead-host
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["host_id"],
required: ["host_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { host_id: {
host_id: { $ref: "common#/properties/id",
$ref: "common#/properties/id", },
}, expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand",
},
}, },
}, },
{ },
host_id: req.params.host_id, {
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, host_id: req.params.host_id,
}, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
); },
const row = await internalDeadHost.get(res.locals.access, { )
id: Number.parseInt(data.host_id, 10), .then((data) => {
expand: data.expand, return internalDeadHost.get(res.locals.access, {
}); id: Number.parseInt(data.host_id, 10),
res.status(200).send(row); expand: data.expand,
} catch (err) { });
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((row) => {
} res.status(200).send(row);
})
.catch(next);
}) })
/** /**
* PUT /api/nginx/dead-hosts/123 * PUT /api/nginx/dead-hosts/123
* *
* Update an existing dead-host * Update and existing dead-host
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/dead-hosts/{hostID}", "put"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts/{hostID}", "put"), req.body); .then((payload) => {
payload.id = Number.parseInt(req.params.host_id, 10); payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalDeadHost.update(res.locals.access, payload); return internalDeadHost.update(res.locals.access, payload);
res.status(200).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(200).send(result);
next(err); })
} .catch(next);
}) })
/** /**
* DELETE /api/nginx/dead-hosts/123 * DELETE /api/nginx/dead-hosts/123
* *
* Delete a dead-host * Update and existing dead-host
*/ */
.delete(async (req, res, next) => { .delete((req, res, next) => {
try { internalDeadHost
const result = await internalDeadHost.delete(res.locals.access, { .delete(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -167,16 +163,13 @@ router
/** /**
* POST /api/nginx/dead-hosts/123/enable * POST /api/nginx/dead-hosts/123/enable
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalDeadHost
const result = await internalDeadHost.enable(res.locals.access, { .enable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -195,13 +188,12 @@ router
* POST /api/nginx/dead-hosts/123/disable * POST /api/nginx/dead-hosts/123/disable
*/ */
.post((req, res, next) => { .post((req, res, next) => {
try { internalDeadHost
const result = internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) }); .disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
res.status(200).send(result); .then((result) => {
} catch (err) { res.status(200).send(result);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .catch(next);
}
}); });
export default router; export default router;

View File

@@ -3,7 +3,6 @@ import internalProxyHost from "../../internal/proxy-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js"; import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js"; import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js"; import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js"; import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -27,31 +26,31 @@ router
* *
* Retrieve all proxy-hosts * Retrieve all proxy-hosts
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, {
query: typeof req.query.query === "string" ? req.query.query : 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 internalProxyHost.getAll(res.locals.access, data.expand, data.query); )
res.status(200).send(rows); .then((data) => {
} catch (err) { return internalProxyHost.getAll(res.locals.access, data.expand, data.query);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((rows) => {
} res.status(200).send(rows);
})
.catch(next);
}) })
/** /**
@@ -59,15 +58,15 @@ router
* *
* Create a new proxy-host * Create a new proxy-host
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body); .then((payload) => {
const result = await internalProxyHost.create(res.locals.access, payload); return internalProxyHost.create(res.locals.access, payload);
res.status(201).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(201).send(result);
next(err); })
} .catch(next);
}); });
/** /**
@@ -87,35 +86,35 @@ router
* *
* Retrieve a specific proxy-host * Retrieve a specific proxy-host
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["host_id"],
required: ["host_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { host_id: {
host_id: { $ref: "common#/properties/id",
$ref: "common#/properties/id", },
}, expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand",
},
}, },
}, },
{ },
host_id: req.params.host_id, {
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, host_id: req.params.host_id,
}, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
); },
const row = await internalProxyHost.get(res.locals.access, { )
id: Number.parseInt(data.host_id, 10), .then((data) => {
expand: data.expand, return internalProxyHost.get(res.locals.access, {
}); id: Number.parseInt(data.host_id, 10),
res.status(200).send(row); expand: data.expand,
} catch (err) { });
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((row) => {
} res.status(200).send(row);
})
.catch(next);
}) })
/** /**
@@ -123,16 +122,16 @@ router
* *
* Update and existing proxy-host * Update and existing proxy-host
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/proxy-hosts/{hostID}", "put"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts/{hostID}", "put"), req.body); .then((payload) => {
payload.id = Number.parseInt(req.params.host_id, 10); payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalProxyHost.update(res.locals.access, payload); return internalProxyHost.update(res.locals.access, payload);
res.status(200).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(200).send(result);
next(err); })
} .catch(next);
}) })
/** /**
@@ -140,16 +139,13 @@ router
* *
* Update and existing proxy-host * Update and existing proxy-host
*/ */
.delete(async (req, res, next) => { .delete((req, res, next) => {
try { internalProxyHost
const result = await internalProxyHost.delete(res.locals.access, { .delete(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -167,16 +163,13 @@ router
/** /**
* POST /api/nginx/proxy-hosts/123/enable * POST /api/nginx/proxy-hosts/123/enable
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalProxyHost
const result = await internalProxyHost.enable(res.locals.access, { .enable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -194,16 +187,13 @@ router
/** /**
* POST /api/nginx/proxy-hosts/123/disable * POST /api/nginx/proxy-hosts/123/disable
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalProxyHost
const result = await internalProxyHost.disable(res.locals.access, { .disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
export default router; export default router;

View File

@@ -3,7 +3,6 @@ import internalRedirectionHost from "../../internal/redirection-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js"; import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js"; import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js"; import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js"; import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -27,31 +26,31 @@ router
* *
* Retrieve all redirection-hosts * Retrieve all redirection-hosts
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, {
query: typeof req.query.query === "string" ? req.query.query : 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 internalRedirectionHost.getAll(res.locals.access, data.expand, data.query); )
res.status(200).send(rows); .then((data) => {
} catch (err) { return internalRedirectionHost.getAll(res.locals.access, data.expand, data.query);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((rows) => {
} res.status(200).send(rows);
})
.catch(next);
}) })
/** /**
@@ -59,15 +58,15 @@ router
* *
* Create a new redirection-host * Create a new redirection-host
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/redirection-hosts", "post"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/redirection-hosts", "post"), req.body); .then((payload) => {
const result = await internalRedirectionHost.create(res.locals.access, payload); return internalRedirectionHost.create(res.locals.access, payload);
res.status(201).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(201).send(result);
next(err); })
} .catch(next);
}); });
/** /**
@@ -87,35 +86,35 @@ router
* *
* Retrieve a specific redirection-host * Retrieve a specific redirection-host
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["host_id"],
required: ["host_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { host_id: {
host_id: { $ref: "common#/properties/id",
$ref: "common#/properties/id", },
}, expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand",
},
}, },
}, },
{ },
host_id: req.params.host_id, {
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, host_id: req.params.host_id,
}, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
); },
const row = await internalRedirectionHost.get(res.locals.access, { )
id: Number.parseInt(data.host_id, 10), .then((data) => {
expand: data.expand, return internalRedirectionHost.get(res.locals.access, {
}); id: Number.parseInt(data.host_id, 10),
res.status(200).send(row); expand: data.expand,
} catch (err) { });
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((row) => {
} res.status(200).send(row);
})
.catch(next);
}) })
/** /**
@@ -123,19 +122,16 @@ router
* *
* Update and existing redirection-host * Update and existing redirection-host
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/redirection-hosts/{hostID}", "put"), req.body)
const payload = await apiValidator( .then((payload) => {
getValidationSchema("/nginx/redirection-hosts/{hostID}", "put"), payload.id = Number.parseInt(req.params.host_id, 10);
req.body, return internalRedirectionHost.update(res.locals.access, payload);
); })
payload.id = Number.parseInt(req.params.host_id, 10); .then((result) => {
const result = await internalRedirectionHost.update(res.locals.access, payload); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}) })
/** /**
@@ -143,16 +139,13 @@ router
* *
* Update and existing redirection-host * Update and existing redirection-host
*/ */
.delete(async (req, res, next) => { .delete((req, res, next) => {
try { internalRedirectionHost
const result = await internalRedirectionHost.delete(res.locals.access, { .delete(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -170,16 +163,13 @@ router
/** /**
* POST /api/nginx/redirection-hosts/123/enable * POST /api/nginx/redirection-hosts/123/enable
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalRedirectionHost
const result = await internalRedirectionHost.enable(res.locals.access, { .enable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -197,16 +187,13 @@ router
/** /**
* POST /api/nginx/redirection-hosts/123/disable * POST /api/nginx/redirection-hosts/123/disable
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalRedirectionHost
const result = await internalRedirectionHost.disable(res.locals.access, { .disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
export default router; export default router;

View File

@@ -3,7 +3,6 @@ import internalStream from "../../internal/stream.js";
import jwtdecode from "../../lib/express/jwt-decode.js"; import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js"; import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js"; import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js"; import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -27,31 +26,31 @@ router
* *
* Retrieve all streams * Retrieve all streams
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, {
query: typeof req.query.query === "string" ? req.query.query : 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 internalStream.getAll(res.locals.access, data.expand, data.query); )
res.status(200).send(rows); .then((data) => {
} catch (err) { return internalStream.getAll(res.locals.access, data.expand, data.query);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((rows) => {
} res.status(200).send(rows);
})
.catch(next);
}) })
/** /**
@@ -59,15 +58,15 @@ router
* *
* Create a new stream * Create a new stream
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/streams", "post"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/streams", "post"), req.body); .then((payload) => {
const result = await internalStream.create(res.locals.access, payload); return internalStream.create(res.locals.access, payload);
res.status(201).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(201).send(result);
next(err); })
} .catch(next);
}); });
/** /**
@@ -87,35 +86,35 @@ router
* *
* Retrieve a specific stream * Retrieve a specific stream
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["stream_id"],
required: ["stream_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { stream_id: {
stream_id: { $ref: "common#/properties/id",
$ref: "common#/properties/id", },
}, expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand",
},
}, },
}, },
{ },
stream_id: req.params.stream_id, {
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, stream_id: req.params.stream_id,
}, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
); },
const row = await internalStream.get(res.locals.access, { )
id: Number.parseInt(data.stream_id, 10), .then((data) => {
expand: data.expand, return internalStream.get(res.locals.access, {
}); id: Number.parseInt(data.stream_id, 10),
res.status(200).send(row); expand: data.expand,
} catch (err) { });
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((row) => {
} res.status(200).send(row);
})
.catch(next);
}) })
/** /**
@@ -123,16 +122,16 @@ router
* *
* Update and existing stream * Update and existing stream
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/nginx/streams/{streamID}", "put"), req.body)
const payload = await apiValidator(getValidationSchema("/nginx/streams/{streamID}", "put"), req.body); .then((payload) => {
payload.id = Number.parseInt(req.params.stream_id, 10); payload.id = Number.parseInt(req.params.stream_id, 10);
const result = await internalStream.update(res.locals.access, payload); return internalStream.update(res.locals.access, payload);
res.status(200).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(200).send(result);
next(err); })
} .catch(next);
}) })
/** /**
@@ -140,16 +139,13 @@ router
* *
* Update and existing stream * Update and existing stream
*/ */
.delete(async (req, res, next) => { .delete((req, res, next) => {
try { internalStream
const result = await internalStream.delete(res.locals.access, { .delete(res.locals.access, { id: Number.parseInt(req.params.stream_id, 10) })
id: Number.parseInt(req.params.stream_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -167,16 +163,13 @@ router
/** /**
* POST /api/nginx/streams/123/enable * POST /api/nginx/streams/123/enable
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalStream
const result = await internalStream.enable(res.locals.access, { .enable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -194,16 +187,13 @@ router
/** /**
* POST /api/nginx/streams/123/disable * POST /api/nginx/streams/123/disable
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalStream
const result = await internalStream.disable(res.locals.access, { .disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) })
id: Number.parseInt(req.params.host_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
export default router; export default router;

View File

@@ -1,7 +1,6 @@
import express from "express"; import express from "express";
import internalReport from "../internal/report.js"; import internalReport from "../internal/report.js";
import jwtdecode from "../lib/express/jwt-decode.js"; import jwtdecode from "../lib/express/jwt-decode.js";
import { express as logger } from "../logger.js";
const router = express.Router({ const router = express.Router({
caseSensitive: true, caseSensitive: true,
@@ -14,19 +13,17 @@ router
.options((_, res) => { .options((_, res) => {
res.sendStatus(204); res.sendStatus(204);
}) })
.all(jwtdecode())
/** /**
* GET /reports/hosts * GET /reports/hosts
*/ */
.get(async (req, res, next) => { .get(jwtdecode(), (_, res, next) => {
try { internalReport
const data = await internalReport.getHostsReport(res.locals.access); .getHostsReport(res.locals.access)
res.status(200).send(data); .then((data) => {
} catch (err) { res.status(200).send(data);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .catch(next);
}
}); });
export default router; export default router;

View File

@@ -1,5 +1,4 @@
import express from "express"; import express from "express";
import { express as logger } from "../logger.js";
import PACKAGE from "../package.json" with { type: "json" }; import PACKAGE from "../package.json" with { type: "json" };
import { getCompiledSchema } from "../schema/index.js"; import { getCompiledSchema } from "../schema/index.js";
@@ -19,26 +18,21 @@ router
* GET /schema * GET /schema
*/ */
.get(async (req, res) => { .get(async (req, res) => {
try { const swaggerJSON = await getCompiledSchema();
const swaggerJSON = await getCompiledSchema();
let proto = req.protocol; let proto = req.protocol;
if (typeof req.headers["x-forwarded-proto"] !== "undefined" && req.headers["x-forwarded-proto"]) { if (typeof req.headers["x-forwarded-proto"] !== "undefined" && req.headers["x-forwarded-proto"]) {
proto = req.headers["x-forwarded-proto"]; proto = req.headers["x-forwarded-proto"];
}
let origin = `${proto}://${req.hostname}`;
if (typeof req.headers.origin !== "undefined" && req.headers.origin) {
origin = req.headers.origin;
}
swaggerJSON.info.version = PACKAGE.version;
swaggerJSON.servers[0].url = `${origin}/api`;
res.status(200).send(swaggerJSON);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
} }
let origin = `${proto}://${req.hostname}`;
if (typeof req.headers.origin !== "undefined" && req.headers.origin) {
origin = req.headers.origin;
}
swaggerJSON.info.version = PACKAGE.version;
swaggerJSON.servers[0].url = `${origin}/api`;
res.status(200).send(swaggerJSON);
}); });
export default router; export default router;

View File

@@ -3,7 +3,6 @@ import internalSetting from "../internal/setting.js";
import jwtdecode from "../lib/express/jwt-decode.js"; import jwtdecode from "../lib/express/jwt-decode.js";
import apiValidator from "../lib/validator/api.js"; import apiValidator from "../lib/validator/api.js";
import validator from "../lib/validator/index.js"; import validator from "../lib/validator/index.js";
import { express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js"; import { getValidationSchema } from "../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -27,14 +26,13 @@ router
* *
* Retrieve all settings * Retrieve all settings
*/ */
.get(async (req, res, next) => { .get((_, res, next) => {
try { internalSetting
const rows = await internalSetting.getAll(res.locals.access); .getAll(res.locals.access)
res.status(200).send(rows); .then((rows) => {
} catch (err) { res.status(200).send(rows);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .catch(next);
}
}); });
/** /**
@@ -54,31 +52,31 @@ router
* *
* Retrieve a specific setting * Retrieve a specific setting
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["setting_id"],
required: ["setting_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { setting_id: {
setting_id: { type: "string",
type: "string", minLength: 1,
minLength: 1,
},
}, },
}, },
{ },
setting_id: req.params.setting_id, {
}, setting_id: req.params.setting_id,
); },
const row = await internalSetting.get(res.locals.access, { )
id: data.setting_id, .then((data) => {
}); return internalSetting.get(res.locals.access, {
res.status(200).send(row); id: data.setting_id,
} catch (err) { });
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .then((row) => {
} res.status(200).send(row);
})
.catch(next);
}) })
/** /**
@@ -86,16 +84,16 @@ router
* *
* Update and existing setting * Update and existing setting
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/settings/{settingID}", "put"), req.body)
const payload = await apiValidator(getValidationSchema("/settings/{settingID}", "put"), req.body); .then((payload) => {
payload.id = req.params.setting_id; payload.id = req.params.setting_id;
const result = await internalSetting.update(res.locals.access, payload); return internalSetting.update(res.locals.access, payload);
res.status(200).send(result); })
} catch (err) { .then((result) => {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); res.status(200).send(result);
next(err); })
} .catch(next);
}); });
export default router; export default router;

View File

@@ -2,7 +2,6 @@ import express from "express";
import internalToken from "../internal/token.js"; import internalToken from "../internal/token.js";
import jwtdecode from "../lib/express/jwt-decode.js"; import jwtdecode from "../lib/express/jwt-decode.js";
import apiValidator from "../lib/validator/api.js"; import apiValidator from "../lib/validator/api.js";
import { express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js"; import { getValidationSchema } from "../schema/index.js";
const router = express.Router({ const router = express.Router({
@@ -24,17 +23,16 @@ router
* We also piggy back on to this method, allowing admins to get tokens * We also piggy back on to this method, allowing admins to get tokens
* for services like Job board and Worker. * for services like Job board and Worker.
*/ */
.get(jwtdecode(), async (req, res, next) => { .get(jwtdecode(), (req, res, next) => {
try { internalToken
const data = await internalToken.getFreshToken(res.locals.access, { .getFreshToken(res.locals.access, {
expiry: typeof req.query.expiry !== "undefined" ? req.query.expiry : null, expiry: typeof req.query.expiry !== "undefined" ? req.query.expiry : null,
scope: typeof req.query.scope !== "undefined" ? req.query.scope : null, scope: typeof req.query.scope !== "undefined" ? req.query.scope : null,
}); })
res.status(200).send(data); .then((data) => {
} catch (err) { res.status(200).send(data);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); })
next(err); .catch(next);
}
}) })
/** /**
@@ -43,14 +41,12 @@ router
* Create a new Token * Create a new Token
*/ */
.post(async (req, res, next) => { .post(async (req, res, next) => {
try { apiValidator(getValidationSchema("/tokens", "post"), req.body)
const data = await apiValidator(getValidationSchema("/tokens", "post"), req.body); .then(internalToken.getTokenFromEmail)
const result = await internalToken.getTokenFromEmail(data); .then((data) => {
res.status(200).send(result); res.status(200).send(data);
} catch (err) { })
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); .catch(next);
next(err);
}
}); });
export default router; export default router;

View File

@@ -1,15 +1,10 @@
import express from "express"; import express from "express";
import internalUser from "../internal/user.js"; import internalUser from "../internal/user.js";
import Access from "../lib/access.js";
import { isCI } from "../lib/config.js";
import errs from "../lib/error.js";
import jwtdecode from "../lib/express/jwt-decode.js"; import jwtdecode from "../lib/express/jwt-decode.js";
import userIdFromMe from "../lib/express/user-id-from-me.js"; import userIdFromMe from "../lib/express/user-id-from-me.js";
import apiValidator from "../lib/validator/api.js"; import apiValidator from "../lib/validator/api.js";
import validator from "../lib/validator/index.js"; import validator from "../lib/validator/index.js";
import { express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js"; import { getValidationSchema } from "../schema/index.js";
import { isSetup } from "../setup.js";
const router = express.Router({ const router = express.Router({
caseSensitive: true, caseSensitive: true,
@@ -32,38 +27,35 @@ router
* *
* Retrieve all users * Retrieve all users
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ additionalProperties: false,
additionalProperties: false, properties: {
properties: { expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand", },
}, query: {
query: { $ref: "common#/properties/query",
$ref: "common#/properties/query",
},
}, },
}, },
{ },
expand: {
typeof req.query.expand === "string" expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
? req.query.expand.split(",") query: typeof req.query.query === "string" ? req.query.query : null,
: null, },
query: typeof req.query.query === "string" ? req.query.query : null, )
}, .then((data) => {
); return internalUser.getAll(res.locals.access, data.expand, data.query);
const users = await internalUser.getAll( })
res.locals.access, .then((users) => {
data.expand, res.status(200).send(users);
data.query, })
); .catch((err) => {
res.status(200).send(users); console.log(err);
} catch (err) { next(err);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`); });
next(err); //.catch(next);
}
}) })
/** /**
@@ -71,66 +63,15 @@ router
* *
* Create a new User * Create a new User
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
const body = req.body; apiValidator(getValidationSchema("/users", "post"), req.body)
.then((payload) => {
try { return internalUser.create(res.locals.access, payload);
// If we are in setup mode, we don't check access for current user })
const setup = await isSetup(); .then((result) => {
if (!setup) { res.status(201).send(result);
logger.info("Creating a new user in setup mode"); })
const access = new Access(null); .catch(next);
await access.load(true);
res.locals.access = access;
// We are in setup mode, set some defaults for this first new user, such as making
// them an admin.
body.is_disabled = false;
if (typeof body.roles !== "object" || body.roles === null) {
body.roles = [];
}
if (body.roles.indexOf("admin") === -1) {
body.roles.push("admin");
}
}
const payload = await apiValidator(
getValidationSchema("/users", "post"),
body,
);
const user = await internalUser.create(res.locals.access, payload);
res.status(201).send(user);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/users
*
* Deletes ALL users. This is NOT GENERALLY AVAILABLE!
* (!) It is NOT an authenticated endpoint.
* (!) Only CI should be able to call this endpoint. As a result,
*
* it will only work when the env vars DEBUG=true and CI=true
*
* Do NOT set those env vars in a production environment!
*/
.delete(async (_, res, next) => {
if (isCI()) {
try {
logger.warn("Deleting all users - CI environment detected, allowing this operation");
await internalUser.deleteAll();
res.status(200).send(true);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
return;
}
next(new errs.ItemNotFoundError());
}); });
/** /**
@@ -151,43 +92,39 @@ router
* *
* Retrieve a specific user * Retrieve a specific user
*/ */
.get(async (req, res, next) => { .get((req, res, next) => {
try { validator(
const data = await validator( {
{ required: ["user_id"],
required: ["user_id"], additionalProperties: false,
additionalProperties: false, properties: {
properties: { user_id: {
user_id: { $ref: "common#/properties/id",
$ref: "common#/properties/id", },
}, expand: {
expand: { $ref: "common#/properties/expand",
$ref: "common#/properties/expand",
},
}, },
}, },
{ },
user_id: req.params.user_id, {
expand: user_id: req.params.user_id,
typeof req.query.expand === "string" expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
? req.query.expand.split(",") },
: null, )
}, .then((data) => {
); return internalUser.get(res.locals.access, {
id: data.user_id,
const user = await internalUser.get(res.locals.access, { expand: data.expand,
id: data.user_id, omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id),
expand: data.expand, });
omit: internalUser.getUserOmisionsByAccess( })
res.locals.access, .then((user) => {
data.user_id, res.status(200).send(user);
), })
.catch((err) => {
console.log(err);
next(err);
}); });
res.status(200).send(user);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}) })
/** /**
@@ -195,19 +132,16 @@ router
* *
* Update and existing user * Update and existing user
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/users/{userID}", "put"), req.body)
const payload = await apiValidator( .then((payload) => {
getValidationSchema("/users/{userID}", "put"), payload.id = req.params.user_id;
req.body, return internalUser.update(res.locals.access, payload);
); })
payload.id = req.params.user_id; .then((result) => {
const result = await internalUser.update(res.locals.access, payload); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}) })
/** /**
@@ -215,16 +149,13 @@ router
* *
* Update and existing user * Update and existing user
*/ */
.delete(async (req, res, next) => { .delete((req, res, next) => {
try { internalUser
const result = await internalUser.delete(res.locals.access, { .delete(res.locals.access, { id: req.params.user_id })
id: req.params.user_id, .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -245,19 +176,16 @@ router
* *
* Update password for a user * Update password for a user
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/users/{userID}/auth", "put"), req.body)
const payload = await apiValidator( .then((payload) => {
getValidationSchema("/users/{userID}/auth", "put"), payload.id = req.params.user_id;
req.body, return internalUser.setPassword(res.locals.access, payload);
); })
payload.id = req.params.user_id; .then((result) => {
const result = await internalUser.setPassword(res.locals.access, payload); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -278,22 +206,16 @@ router
* *
* Set some or all permissions for a user * Set some or all permissions for a user
*/ */
.put(async (req, res, next) => { .put((req, res, next) => {
try { apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body)
const payload = await apiValidator( .then((payload) => {
getValidationSchema("/users/{userID}/permissions", "put"), payload.id = req.params.user_id;
req.body, return internalUser.setPermissions(res.locals.access, payload);
); })
payload.id = req.params.user_id; .then((result) => {
const result = await internalUser.setPermissions( res.status(200).send(result);
res.locals.access, })
payload, .catch(next);
);
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
/** /**
@@ -313,16 +235,13 @@ router
* *
* Log in as a user * Log in as a user
*/ */
.post(async (req, res, next) => { .post((req, res, next) => {
try { internalUser
const result = await internalUser.loginAs(res.locals.access, { .loginAs(res.locals.access, { id: Number.parseInt(req.params.user_id, 10) })
id: Number.parseInt(req.params.user_id, 10), .then((result) => {
}); res.status(200).send(result);
res.status(200).send(result); })
} catch (err) { .catch(next);
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
}); });
export default router; export default router;

View File

@@ -1,7 +0,0 @@
{
"type": "array",
"description": "Audit Log list",
"items": {
"$ref": "./audit-log-object.json"
}
}

View File

@@ -1,16 +1,7 @@
{ {
"type": "object", "type": "object",
"description": "Audit Log object", "description": "Audit Log object",
"required": [ "required": ["id", "created_on", "modified_on", "user_id", "object_type", "object_id", "action", "meta"],
"id",
"created_on",
"modified_on",
"user_id",
"object_type",
"object_id",
"action",
"meta"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"id": { "id": {
@@ -36,9 +27,6 @@
}, },
"meta": { "meta": {
"type": "object" "type": "object"
},
"user": {
"$ref": "./user-object.json"
} }
} }
} }

View File

@@ -62,9 +62,15 @@
"dns_provider_credentials": { "dns_provider_credentials": {
"type": "string" "type": "string"
}, },
"letsencrypt_agree": {
"type": "boolean"
},
"letsencrypt_certificate": { "letsencrypt_certificate": {
"type": "object" "type": "object"
}, },
"letsencrypt_email": {
"$ref": "../common.json#/properties/email"
},
"propagation_seconds": { "propagation_seconds": {
"type": "integer", "type": "integer",
"minimum": 0 "minimum": 0

View File

@@ -9,11 +9,6 @@
"description": "Healthy", "description": "Healthy",
"example": "OK" "example": "OK"
}, },
"setup": {
"type": "boolean",
"description": "Whether the initial setup has been completed",
"example": true
},
"version": { "version": {
"type": "object", "type": "object",
"description": "The version object", "description": "The version object",

View File

@@ -31,7 +31,7 @@
}, },
{ {
"type": "string", "type": "string",
"format": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$" "format": "ipv4"
}, },
{ {
"type": "string", "type": "string",

View File

@@ -54,63 +54,6 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"permissions": {
"type": "object",
"description": "Permissions if expanded in request",
"required": [
"visibility",
"proxy_hosts",
"redirection_hosts",
"dead_hosts",
"streams",
"access_lists",
"certificates"
],
"properties": {
"visibility": {
"type": "string",
"description": "Visibility level",
"example": "all",
"pattern": "^(all|user)$"
},
"proxy_hosts": {
"type": "string",
"description": "Proxy Hosts access level",
"example": "all",
"pattern": "^(manage|view|hidden)$"
},
"redirection_hosts": {
"type": "string",
"description": "Redirection Hosts access level",
"example": "all",
"pattern": "^(manage|view|hidden)$"
},
"dead_hosts": {
"type": "string",
"description": "Dead Hosts access level",
"example": "all",
"pattern": "^(manage|view|hidden)$"
},
"streams": {
"type": "string",
"description": "Streams access level",
"example": "all",
"pattern": "^(manage|view|hidden)$"
},
"access_lists": {
"type": "string",
"description": "Access Lists access level",
"example": "all",
"pattern": "^(manage|view|hidden)$"
},
"certificates": {
"type": "string",
"description": "Certificates access level",
"example": "all",
"pattern": "^(manage|view|hidden)$"
}
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"operationId": "getAuditLogs", "operationId": "getAuditLog",
"summary": "Get Audit Logs", "summary": "Get Audit Log",
"tags": ["Audit Log"], "tags": ["Audit Log"],
"security": [ "security": [
{ {
@@ -44,7 +44,7 @@
} }
}, },
"schema": { "schema": {
"$ref": "../../components/audit-log-list.json" "$ref": "../../components/audit-log-object.json"
} }
} }
} }

View File

@@ -1,73 +0,0 @@
{
"operationId": "getAuditLog",
"summary": "Get Audit Log Event",
"tags": [
"Audit Log"
],
"security": [
{
"BearerAuth": [
"audit-log"
]
}
],
"parameters": [
{
"in": "path",
"name": "id",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"id": 1,
"created_on": "2025-09-15T17:27:45.000Z",
"modified_on": "2025-09-15T17:27:45.000Z",
"user_id": 1,
"object_type": "user",
"object_id": 1,
"action": "created",
"meta": {
"id": 1,
"created_on": "2025-09-15T17:27:45.000Z",
"modified_on": "2025-09-15T17:27:45.000Z",
"is_disabled": false,
"email": "jc@jc21.com",
"name": "Jamie",
"nickname": "Jamie",
"avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm",
"roles": [
"admin"
],
"permissions": {
"visibility": "all",
"proxy_hosts": "manage",
"redirection_hosts": "manage",
"dead_hosts": "manage",
"streams": "manage",
"access_lists": "manage",
"certificates": "manage"
}
}
}
}
},
"schema": {
"$ref": "../../../components/audit-log-object.json"
}
}
}
}
}
}

View File

@@ -11,7 +11,6 @@
"default": { "default": {
"value": { "value": {
"status": "OK", "status": "OK",
"setup": true,
"version": { "version": {
"major": 2, "major": 2,
"minor": 1, "minor": 1,

View File

@@ -36,6 +36,8 @@
"domain_names": ["test.example.com"], "domain_names": ["test.example.com"],
"expires_on": "2025-01-07T04:34:18.000Z", "expires_on": "2025-01-07T04:34:18.000Z",
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false "dns_challenge": false
} }
} }

View File

@@ -37,6 +37,8 @@
"nice_name": "My Test Cert", "nice_name": "My Test Cert",
"domain_names": ["test.jc21.supernerd.pro"], "domain_names": ["test.jc21.supernerd.pro"],
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false "dns_challenge": false
} }
} }

View File

@@ -36,6 +36,8 @@
"domain_names": ["test.example.com"], "domain_names": ["test.example.com"],
"expires_on": "2025-01-07T04:34:18.000Z", "expires_on": "2025-01-07T04:34:18.000Z",
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false "dns_challenge": false
} }
} }

View File

@@ -52,6 +52,8 @@
"nice_name": "test.example.com", "nice_name": "test.example.com",
"domain_names": ["test.example.com"], "domain_names": ["test.example.com"],
"meta": { "meta": {
"letsencrypt_email": "jc@jc21.com",
"letsencrypt_agree": true,
"dns_challenge": false, "dns_challenge": false,
"letsencrypt_certificate": { "letsencrypt_certificate": {
"cn": "test.example.com", "cn": "test.example.com",

View File

@@ -37,9 +37,6 @@
}, },
"meta": { "meta": {
"$ref": "../../../components/stream-object.json#/properties/meta" "$ref": "../../../components/stream-object.json#/properties/meta"
},
"domain_names": {
"$ref": "../../../components/dead-host-object.json#/properties/domain_names"
} }
} }
} }

View File

@@ -29,11 +29,6 @@
"$ref": "./paths/audit-log/get.json" "$ref": "./paths/audit-log/get.json"
} }
}, },
"/audit-log/{id}": {
"get": {
"$ref": "./paths/audit-log/id/get.json"
}
},
"/nginx/access-lists": { "/nginx/access-lists": {
"get": { "get": {
"$ref": "./paths/nginx/access-lists/get.json" "$ref": "./paths/nginx/access-lists/get.json"

View File

@@ -7,68 +7,65 @@ 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";
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 * Creates a default admin users if one doesn't already exist in the database
* *
* @returns {Promise} * @returns {Promise}
*/ */
const setupDefaultUser = async () => { const setupDefaultUser = () => {
const initialAdminEmail = process.env.INITIAL_ADMIN_EMAIL; return userModel
const initialAdminPassword = process.env.INITIAL_ADMIN_PASSWORD; .query()
.select("id")
.where("is_deleted", 0)
.first()
.then((row) => {
if (!row || !row.id) {
// Create a new user and set password
const email = (process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com').toLowerCase();
const password = process.env.INITIAL_ADMIN_PASSWORD || "changeme";
// This will only create a new user when there are no active users in the database logger.info(`Creating a new user: ${email} with password: ${password}`);
// and the INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD environment variables are set.
// Otherwise, users should be shown the setup wizard in the frontend.
// I'm keeping this legacy behavior in case some people are automating deployments.
if (!initialAdminEmail || !initialAdminPassword) { const data = {
return Promise.resolve(); is_deleted: 0,
} email: email,
name: "Administrator",
nickname: "Admin",
avatar: "",
roles: ["admin"],
};
const userIsetup = await isSetup(); return userModel
if (!userIsetup) { .query()
// Create a new user and set password .insertAndFetch(data)
logger.info(`Creating a new user: ${initialAdminEmail} with password: ${initialAdminPassword}`); .then((user) => {
return authModel
const data = { .query()
is_deleted: 0, .insert({
email: email, user_id: user.id,
name: "Administrator", type: "password",
nickname: "Admin", secret: password,
avatar: "", meta: {},
roles: ["admin"], })
}; .then(() => {
return userPermissionModel.query().insert({
const user = await userModel user_id: user.id,
.query() visibility: "all",
.insertAndFetch(data); proxy_hosts: "manage",
redirection_hosts: "manage",
await authModel dead_hosts: "manage",
.query() streams: "manage",
.insert({ access_lists: "manage",
user_id: user.id, certificates: "manage",
type: "password", });
secret: password, });
meta: {}, })
}); .then(() => {
logger.info("Initial admin setup completed");
await userPermissionModel.query().insert({ });
user_id: user.id, }
visibility: "all", logger.debug("Admin user setup not required");
proxy_hosts: "manage",
redirection_hosts: "manage",
dead_hosts: "manage",
streams: "manage",
access_lists: "manage",
certificates: "manage",
}); });
logger.info("Initial admin setup completed");
}
}; };
/** /**
@@ -76,25 +73,29 @@ const setupDefaultUser = async () => {
* *
* @returns {Promise} * @returns {Promise}
*/ */
const setupDefaultSettings = async () => { const setupDefaultSettings = () => {
const row = await settingModel return settingModel
.query() .query()
.select("id") .select("id")
.where({ id: "default-site" }) .where({ id: "default-site" })
.first(); .first()
.then((row) => {
if (!row?.id) { if (!row || !row.id) {
await settingModel settingModel
.query() .query()
.insert({ .insert({
id: "default-site", id: "default-site",
name: "Default Site", name: "Default Site",
description: "What to show when Nginx is hit with an unknown Host", description: "What to show when Nginx is hit with an unknown Host",
value: "congratulations", value: "congratulations",
meta: {}, meta: {},
}); })
logger.info("Default settings added"); .then(() => {
} logger.info("Default settings added");
});
}
logger.debug("Default setting setup not required");
});
}; };
/** /**
@@ -102,43 +103,43 @@ const setupDefaultSettings = async () => {
* *
* @returns {Promise} * @returns {Promise}
*/ */
const setupCertbotPlugins = async () => { const setupCertbotPlugins = () => {
const certificates = await certificateModel return certificateModel
.query() .query()
.where("is_deleted", 0) .where("is_deleted", 0)
.andWhere("provider", "letsencrypt"); .andWhere("provider", "letsencrypt")
.then((certificates) => {
if (certificates?.length) {
const plugins = [];
const promises = [];
if (certificates?.length) { certificates.map((certificate) => {
const plugins = []; if (certificate.meta && certificate.meta.dns_challenge === true) {
const promises = []; if (plugins.indexOf(certificate.meta.dns_provider) === -1) {
plugins.push(certificate.meta.dns_provider);
}
certificates.map((certificate) => { // Make sure credentials file exists
if (certificate.meta && certificate.meta.dns_challenge === true) { const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
if (plugins.indexOf(certificate.meta.dns_provider) === -1) { // Escape single quotes and backslashes
plugins.push(certificate.meta.dns_provider); const escapedCredentials = certificate.meta.dns_provider_credentials
} .replaceAll("'", "\\'")
.replaceAll("\\", "\\\\");
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;
promises.push(utils.exec(credentials_cmd));
}
return true;
});
// Make sure credentials file exists return installPlugins(plugins).then(() => {
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; if (promises.length) {
// Escape single quotes and backslashes return Promise.all(promises).then(() => {
if (typeof certificate.meta.dns_provider_credentials === "string") { logger.info(`Added Certbot plugins ${plugins.join(", ")}`);
const escapedCredentials = certificate.meta.dns_provider_credentials });
.replaceAll("'", "\\'") }
.replaceAll("\\", "\\\\"); });
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;
promises.push(utils.exec(credentials_cmd));
}
} }
return true;
}); });
await installPlugins(plugins);
if (promises.length) {
await Promise.all(promises);
logger.info(`Added Certbot plugins ${plugins.join(", ")}`);
}
}
}; };
/** /**

View File

@@ -43,59 +43,59 @@
ajv-draft-04 "^1.0.0" ajv-draft-04 "^1.0.0"
call-me-maybe "^1.0.2" call-me-maybe "^1.0.2"
"@biomejs/biome@^2.2.4": "@biomejs/biome@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.2.4.tgz#184e4b83f89bd0d4151682a5aa3840df37748e17" resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.2.0.tgz#823ba77363651f310c47909747c879791ebd15c9"
integrity sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg== integrity sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==
optionalDependencies: optionalDependencies:
"@biomejs/cli-darwin-arm64" "2.2.4" "@biomejs/cli-darwin-arm64" "2.2.0"
"@biomejs/cli-darwin-x64" "2.2.4" "@biomejs/cli-darwin-x64" "2.2.0"
"@biomejs/cli-linux-arm64" "2.2.4" "@biomejs/cli-linux-arm64" "2.2.0"
"@biomejs/cli-linux-arm64-musl" "2.2.4" "@biomejs/cli-linux-arm64-musl" "2.2.0"
"@biomejs/cli-linux-x64" "2.2.4" "@biomejs/cli-linux-x64" "2.2.0"
"@biomejs/cli-linux-x64-musl" "2.2.4" "@biomejs/cli-linux-x64-musl" "2.2.0"
"@biomejs/cli-win32-arm64" "2.2.4" "@biomejs/cli-win32-arm64" "2.2.0"
"@biomejs/cli-win32-x64" "2.2.4" "@biomejs/cli-win32-x64" "2.2.0"
"@biomejs/cli-darwin-arm64@2.2.4": "@biomejs/cli-darwin-arm64@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz#9b50620c93501e370b7e6d5a8445f117f9946a0c" resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.0.tgz#1abf9508e7d0776871710687ddad36e692dce3bc"
integrity sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA== integrity sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg==
"@biomejs/cli-darwin-x64@2.2.4": "@biomejs/cli-darwin-x64@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz#343620c884fc8141155d114430e80e4eacfddc9e" resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.0.tgz#3a51aa569505fedd3a32bb914d608ec27d87f26d"
integrity sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg== integrity sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==
"@biomejs/cli-linux-arm64-musl@2.2.4": "@biomejs/cli-linux-arm64-musl@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz#cabcdadce2bc88b697f4063374224266c6f8b6e5" resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.0.tgz#4d720930732a825b7a8c7cfe1741aec9e7d5ae1d"
integrity sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ== integrity sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==
"@biomejs/cli-linux-arm64@2.2.4": "@biomejs/cli-linux-arm64@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz#55620f8f088145e62e1158eb85c568554d0c8673" resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.0.tgz#d0a5c153ff9243b15600781947d70d6038226feb"
integrity sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw== integrity sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==
"@biomejs/cli-linux-x64-musl@2.2.4": "@biomejs/cli-linux-x64-musl@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz#6bfaea72505afdbda66a66c998d2d169a8b55f90" resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.0.tgz#946095b0a444f395b2df9244153e1cd6b07404c0"
integrity sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg== integrity sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==
"@biomejs/cli-linux-x64@2.2.4": "@biomejs/cli-linux-x64@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz#8c1ed61dcafb8a5939346c714ec122651f57e1db" resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.0.tgz#ae01e0a70c7cd9f842c77dfb4ebd425734667a34"
integrity sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ== integrity sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==
"@biomejs/cli-win32-arm64@2.2.4": "@biomejs/cli-win32-arm64@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz#b2528f6c436e753d6083d7779f0662e08786cedb" resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.0.tgz#09a3988b9d4bab8b8b3a41b4de9560bf70943964"
integrity sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ== integrity sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==
"@biomejs/cli-win32-x64@2.2.4": "@biomejs/cli-win32-x64@2.2.0":
version "2.2.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz#c8e21413120fe073fa49b78fdd987022941ff66f" resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.0.tgz#5d2523b421d847b13fac146cf745436ea8a72b95"
integrity sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg== integrity sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==
"@gar/promisify@^1.0.1": "@gar/promisify@^1.0.1":
version "1.1.3" version "1.1.3"

View File

@@ -15,7 +15,7 @@ ENV SUPPRESS_NO_CONFIG_WARNING=1 \
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
&& apt-get update \ && apt-get update \
&& apt-get install -y jq python3-pip logrotate moreutils \ && apt-get install -y jq python3-pip logrotate \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@@ -7,9 +7,7 @@ services:
fullstack: fullstack:
image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}"
environment: environment:
TZ: "${TZ:-Australia/Brisbane}"
DEBUG: 'true' DEBUG: 'true'
CI: 'true'
FORCE_COLOR: 1 FORCE_COLOR: 1
# Required for DNS Certificate provisioning in CI # Required for DNS Certificate provisioning in CI
LE_SERVER: 'https://ca.internal/acme/acme/directory' LE_SERVER: 'https://ca.internal/acme/acme/directory'

View File

@@ -18,7 +18,6 @@ services:
- website2.example.com - website2.example.com
- website3.example.com - website3.example.com
environment: environment:
TZ: "${TZ:-Australia/Brisbane}"
PUID: 1000 PUID: 1000
PGID: 1000 PGID: 1000
FORCE_COLOR: 1 FORCE_COLOR: 1
@@ -50,9 +49,8 @@ services:
- ../backend:/app - ../backend:/app
- ../frontend:/app/frontend - ../frontend:/app/frontend
- ../global:/app/global - ../global:/app/global
- '/etc/localtime:/etc/localtime:ro'
healthcheck: healthcheck:
test: [ "CMD", "/usr/bin/check-health" ] test: ["CMD", "/usr/bin/check-health"]
interval: 10s interval: 10s
timeout: 3s timeout: 3s
depends_on: depends_on:
@@ -71,14 +69,12 @@ services:
networks: networks:
- nginx_proxy_manager - nginx_proxy_manager
environment: environment:
TZ: "${TZ:-Australia/Brisbane}"
MYSQL_ROOT_PASSWORD: 'npm' MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm' MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm' MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm' MYSQL_PASSWORD: 'npm'
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
- '/etc/localtime:/etc/localtime:ro'
db-postgres: db-postgres:
image: postgres:latest image: postgres:latest
@@ -204,7 +200,7 @@ services:
- nginx_proxy_manager - nginx_proxy_manager
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: [ 'CMD-SHELL', 'redis-cli ping | grep PONG' ] test: ['CMD-SHELL', 'redis-cli ping | grep PONG']
start_period: 20s start_period: 20s
interval: 30s interval: 30s
retries: 5 retries: 5

View File

@@ -5,7 +5,7 @@
"preview": "vitepress preview" "preview": "vitepress preview"
}, },
"devDependencies": { "devDependencies": {
"vitepress": "^1.6.4" "vitepress": "^1.4.0"
}, },
"dependencies": {} "dependencies": {}
} }

View File

@@ -228,13 +228,3 @@ To enable the geoip2 module, you can create the custom configuration file `/data
load_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so; load_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so;
load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so; load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so;
``` ```
## Auto Initial User Creation
Setting these environment variables will create the default user on startup, skipping the UI first user setup screen:
```
environment:
INITIAL_ADMIN_EMAIL: my@example.com
INITIAL_ADMIN_PASSWORD: mypassword1
```

View File

@@ -23,10 +23,4 @@ Your best bet is to ask the [Reddit community for support](https://www.reddit.co
## When adding username and password access control to a proxy host, I can no longer login into the app. ## When adding username and password access control to a proxy host, I can no longer login into the app.
Having an Access Control List (ACL) with username and password requires the browser to always send this username Having an Access Control List (ACL) with username and password requires the browser to always send this username and password in the `Authorization` header on each request. If your proxied app also requires authentication (like Nginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information, as this is the standardized header meant for this kind of information. However having multiples of the same headers is not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps do not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can only be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization.
and password in the `Authorization` header on each request. If your proxied app also requires authentication (like
Nginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information,
as this is the standardized header meant for this kind of information. However having multiples of the same headers
is not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps
do not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can
only be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization.

View File

@@ -35,7 +35,7 @@ so that the barrier for entry here is low.
## Features ## Features
- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.io/) - Beautiful and Secure Admin Interface based on [Tabler](https://tabler.github.io/)
- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx - Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx
- Free SSL using Let's Encrypt or provide your own custom SSL certificates - Free SSL using Let's Encrypt or provide your own custom SSL certificates
- Access Lists and basic HTTP Authentication for your hosts - Access Lists and basic HTTP Authentication for your hosts
@@ -66,8 +66,6 @@ services:
app: app:
image: 'jc21/nginx-proxy-manager:latest' image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped restart: unless-stopped
environment:
TZ: "Australia/Brisbane"
ports: ports:
- '80:80' - '80:80'
- '81:81' - '81:81'
@@ -91,10 +89,17 @@ docker compose up -d
4. Log in to the Admin UI 4. Log in to the Admin UI
When your docker container is running, connect to it on port `81` for the admin interface. When your docker container is running, connect to it on port `81` for the admin interface.
Sometimes this can take a little bit because of the entropy of keys.
[http://127.0.0.1:81](http://127.0.0.1:81) [http://127.0.0.1:81](http://127.0.0.1:81)
This startup can take a minute depending on your hardware. Default Admin User:
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.
## Contributing ## Contributing

View File

@@ -13,7 +13,6 @@ services:
app: app:
image: 'jc21/nginx-proxy-manager:latest' image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped restart: unless-stopped
ports: ports:
# These ports are in format <host-port>:<container-port> # These ports are in format <host-port>:<container-port>
- '80:80' # Public HTTP Port - '80:80' # Public HTTP Port
@@ -22,9 +21,7 @@ services:
# Add any other Stream port you want to expose # Add any other Stream port you want to expose
# - '21:21' # FTP # - '21:21' # FTP
environment: #environment:
TZ: "Australia/Brisbane"
# Uncomment this if you want to change the location of # Uncomment this if you want to change the location of
# the SQLite DB file within the container # the SQLite DB file within the container
# DB_SQLITE_FILE: "/data/database.sqlite" # DB_SQLITE_FILE: "/data/database.sqlite"
@@ -68,7 +65,6 @@ services:
# Add any other Stream port you want to expose # Add any other Stream port you want to expose
# - '21:21' # FTP # - '21:21' # FTP
environment: environment:
TZ: "Australia/Brisbane"
# Mysql/Maria connection parameters: # Mysql/Maria connection parameters:
DB_MYSQL_HOST: "db" DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306 DB_MYSQL_PORT: 3306
@@ -119,7 +115,6 @@ services:
# Add any other Stream port you want to expose # Add any other Stream port you want to expose
# - '21:21' # FTP # - '21:21' # FTP
environment: environment:
TZ: "Australia/Brisbane"
# Postgres parameters: # Postgres parameters:
DB_POSTGRES_HOST: 'db' DB_POSTGRES_HOST: 'db'
DB_POSTGRES_PORT: '5432' DB_POSTGRES_PORT: '5432'
@@ -178,3 +173,21 @@ After the app is running for the first time, the following will happen:
3. A default admin user will be created 3. A default admin user will be created
This process can take a couple of minutes depending on your machine. This process can take a couple of minutes depending on your machine.
## Default Administrator User
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password. You can change defaults with:
```
environment:
INITIAL_ADMIN_EMAIL: my@example.com
INITIAL_ADMIN_PASSWORD: mypassword1
```

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",
@@ -64,8 +64,7 @@
"useUniqueElementIds": "off" "useUniqueElementIds": "off"
}, },
"suspicious": { "suspicious": {
"noExplicitAny": "off", "noExplicitAny": "off"
"noArrayIndexKey": "off"
}, },
"performance": { "performance": {
"noDelete": "off" "noDelete": "off"

View File

@@ -12,51 +12,47 @@
"prettier": "biome format --write ./src", "prettier": "biome format --write ./src",
"locale-extract": "formatjs extract 'src/**/*.tsx'", "locale-extract": "formatjs extract 'src/**/*.tsx'",
"locale-compile": "formatjs compile-folder src/locale/src src/locale/lang", "locale-compile": "formatjs compile-folder src/locale/src src/locale/lang",
"locale-sort": "./src/locale/scripts/locale-sort.sh",
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"@tabler/core": "^1.4.0", "@tabler/core": "^1.4.0",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.34.1",
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.85.6",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@uiw/react-textarea-code-editor": "^3.1.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"country-flag-icons": "^1.5.20", "country-flag-icons": "^1.5.19",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"formik": "^2.4.6", "formik": "^2.4.6",
"generate-password-browser": "^1.1.0",
"humps": "^2.0.1", "humps": "^2.0.1",
"query-string": "^9.3.1", "query-string": "^9.2.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-intl": "^7.1.11", "react-intl": "^7.1.11",
"react-router-dom": "^7.9.1", "react-router-dom": "^7.8.2",
"react-select": "^5.10.2",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"rooks": "^9.3.0" "rooks": "^9.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.2.4", "@biomejs/biome": "2.2.2",
"@formatjs/cli": "^6.7.2", "@formatjs/cli": "^6.7.2",
"@tanstack/react-query-devtools": "^5.89.0", "@tanstack/react-query-devtools": "^5.85.6",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@types/country-flag-icons": "^1.2.2", "@types/country-flag-icons": "^1.2.2",
"@types/humps": "^2.0.6", "@types/humps": "^2.0.6",
"@types/react": "^19.1.13", "@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@types/react-table": "^7.7.20", "@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^5.0.3", "@vitejs/plugin-react": "^5.0.2",
"happy-dom": "^18.0.1", "happy-dom": "^18.0.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"sass": "^1.93.0", "sass": "^1.91.0",
"tmp": "^0.2.5", "tmp": "^0.2.5",
"typescript": "5.9.2", "typescript": "5.9.2",
"vite": "^7.1.6", "vite": "^7.1.4",
"vite-plugin-checker": "^0.10.3", "vite-plugin-checker": "^0.10.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"

View File

@@ -1,14 +1,3 @@
:root {
color-scheme: light dark;
}
.light {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
.modal-backdrop { .modal-backdrop {
--tblr-backdrop-opacity: 0.8 !important; --tblr-backdrop-opacity: 0.8 !important;
} }
@@ -23,54 +12,3 @@
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
.react-select-container {
.react-select__control {
color: var(--tblr-body-color);
background-color: var(--tblr-bg-forms);
border: var(--tblr-border-width) solid var(--tblr-border-color);
.react-select__input {
color: var(--tblr-body-color) !important;
}
.react-select__single-value {
color: var(--tblr-body-color);
}
.react-select__multi-value {
border: 1px solid var(--tblr-border-color);
background-color: var(--tblr-bg-surface-tertiary);
color: var(--tblr-secondary) !important;
.react-select__multi-value__label {
color: var(--tblr-secondary) !important;
}
}
}
.react-select__menu {
background-color: var(--tblr-bg-forms);
.react-select__option {
background: rgba(var(--tblr-primary-rgb), .04);
color: inherit !important;
&.react-select__option--is-focused {
background: rgba(var(--tblr-primary-rgb), .1);
}
&.react-select__option--is-focused.react-select__option--is-selected {
background: rgba(var(--tblr-primary-rgb), .2);
}
}
}
}
.textareaMono {
font-family: 'Courier New', Courier, monospace !important;
resize: vertical;
}
label.row {
cursor: pointer;
}

View File

@@ -13,9 +13,8 @@ import {
import { useAuthState } from "src/context"; import { useAuthState } from "src/context";
import { useHealth } from "src/hooks"; import { useHealth } from "src/hooks";
const Setup = lazy(() => import("src/pages/Setup"));
const Login = lazy(() => import("src/pages/Login"));
const Dashboard = lazy(() => import("src/pages/Dashboard")); const Dashboard = lazy(() => import("src/pages/Dashboard"));
const Login = lazy(() => import("src/pages/Login"));
const Settings = lazy(() => import("src/pages/Settings")); const Settings = lazy(() => import("src/pages/Settings"));
const Certificates = lazy(() => import("src/pages/Certificates")); const Certificates = lazy(() => import("src/pages/Certificates"));
const Access = lazy(() => import("src/pages/Access")); const Access = lazy(() => import("src/pages/Access"));
@@ -38,10 +37,6 @@ function Router() {
return <Unhealthy />; return <Unhealthy />;
} }
if (!health.data?.setup) {
return <Setup />;
}
if (!authenticated) { if (!authenticated) {
return ( return (
<Suspense fallback={<LoadingPage />}> <Suspense fallback={<LoadingPage />}>

View File

@@ -88,19 +88,15 @@ interface PostArgs {
url: string; url: string;
params?: queryString.StringifiableRecord; params?: queryString.StringifiableRecord;
data?: any; data?: any;
noAuth?: boolean;
} }
export async function post({ url, params, data, noAuth }: PostArgs, abortController?: AbortController) { export async function post({ url, params, data }: PostArgs, abortController?: AbortController) {
const apiUrl = buildUrl({ url, params }); const apiUrl = buildUrl({ url, params });
const method = "POST"; const method = "POST";
let headers: Record<string, string> = {}; let headers = {
if (!noAuth) { ...buildAuthHeader(),
headers = { };
...buildAuthHeader(),
};
}
let body: string | FormData | undefined; let body: string | FormData | undefined;
// Check if the data is an instance of FormData // Check if the data is an instance of FormData
@@ -124,7 +120,7 @@ export async function post({ url, params, data, noAuth }: PostArgs, abortControl
interface PutArgs { interface PutArgs {
url: string; url: string;
params?: queryString.StringifiableRecord; params?: queryString.StringifiableRecord;
data?: Record<string, any>; data?: Record<string, unknown>;
} }
export async function put({ url, params, data }: PutArgs, abortController?: AbortController) { export async function put({ url, params, data }: PutArgs, abortController?: AbortController) {
const apiUrl = buildUrl({ url, params }); const apiUrl = buildUrl({ url, params });

View File

@@ -1,10 +1,13 @@
import * as api from "./base"; import * as api from "./base";
import type { AccessList } from "./models"; import type { AccessList } from "./models";
export async function createAccessList(item: AccessList): Promise<AccessList> { export async function createAccessList(item: AccessList, abortController?: AbortController): Promise<AccessList> {
return await api.post({ return await api.post(
url: "/nginx/access-lists", {
// todo: only use whitelist of fields for this data url: "/nginx/access-lists",
data: item, // todo: only use whitelist of fields for this data
}); data: item,
},
abortController,
);
} }

View File

@@ -1,10 +1,13 @@
import * as api from "./base"; import * as api from "./base";
import type { Certificate } from "./models"; import type { Certificate } from "./models";
export async function createCertificate(item: Certificate): Promise<Certificate> { export async function createCertificate(item: Certificate, abortController?: AbortController): Promise<Certificate> {
return await api.post({ return await api.post(
url: "/nginx/certificates", {
// todo: only use whitelist of fields for this data url: "/nginx/certificates",
data: item, // todo: only use whitelist of fields for this data
}); data: item,
},
abortController,
);
} }

View File

@@ -1,10 +1,13 @@
import * as api from "./base"; import * as api from "./base";
import type { DeadHost } from "./models"; import type { DeadHost } from "./models";
export async function createDeadHost(item: DeadHost): Promise<DeadHost> { export async function createDeadHost(item: DeadHost, abortController?: AbortController): Promise<DeadHost> {
return await api.post({ return await api.post(
url: "/nginx/dead-hosts", {
// todo: only use whitelist of fields for this data url: "/nginx/dead-hosts",
data: item, // todo: only use whitelist of fields for this data
}); data: item,
},
abortController,
);
} }

View File

@@ -1,10 +1,13 @@
import * as api from "./base"; import * as api from "./base";
import type { ProxyHost } from "./models"; import type { ProxyHost } from "./models";
export async function createProxyHost(item: ProxyHost): Promise<ProxyHost> { export async function createProxyHost(item: ProxyHost, abortController?: AbortController): Promise<ProxyHost> {
return await api.post({ return await api.post(
url: "/nginx/proxy-hosts", {
// todo: only use whitelist of fields for this data url: "/nginx/proxy-hosts",
data: item, // todo: only use whitelist of fields for this data
}); data: item,
},
abortController,
);
} }

View File

@@ -1,10 +1,16 @@
import * as api from "./base"; import * as api from "./base";
import type { RedirectionHost } from "./models"; import type { RedirectionHost } from "./models";
export async function createRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> { export async function createRedirectionHost(
return await api.post({ item: RedirectionHost,
url: "/nginx/redirection-hosts", abortController?: AbortController,
// todo: only use whitelist of fields for this data ): Promise<RedirectionHost> {
data: item, return await api.post(
}); {
url: "/nginx/redirection-hosts",
// todo: only use whitelist of fields for this data
data: item,
},
abortController,
);
} }

View File

@@ -1,10 +1,13 @@
import * as api from "./base"; import * as api from "./base";
import type { Stream } from "./models"; import type { Stream } from "./models";
export async function createStream(item: Stream): Promise<Stream> { export async function createStream(item: Stream, abortController?: AbortController): Promise<Stream> {
return await api.post({ return await api.post(
url: "/nginx/streams", {
// todo: only use whitelist of fields for this data url: "/nginx/streams",
data: item, // todo: only use whitelist of fields for this data
}); data: item,
},
abortController,
);
} }

View File

@@ -1,25 +1,13 @@
import * as api from "./base"; import * as api from "./base";
import type { User } from "./models"; import type { User } from "./models";
export interface AuthOptions { export async function createUser(item: User, abortController?: AbortController): Promise<User> {
type: string; return await api.post(
secret: string; {
} url: "/users",
// todo: only use whitelist of fields for this data
export interface NewUser { data: item,
name: string; },
nickname: string; abortController,
email: string; );
isDisabled?: boolean;
auth?: AuthOptions;
roles?: string[];
}
export async function createUser(item: NewUser, noAuth?: boolean): Promise<User> {
return await api.post({
url: "/users",
// todo: only use whitelist of fields for this data
data: item,
noAuth,
});
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function deleteAccessList(id: number): Promise<boolean> { export async function deleteAccessList(id: number, abortController?: AbortController): Promise<boolean> {
return await api.del({ return await api.del(
url: `/nginx/access-lists/${id}`, {
}); url: `/nginx/access-lists/${id}`,
},
abortController,
);
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function deleteCertificate(id: number): Promise<boolean> { export async function deleteCertificate(id: number, abortController?: AbortController): Promise<boolean> {
return await api.del({ return await api.del(
url: `/nginx/certificates/${id}`, {
}); url: `/nginx/certificates/${id}`,
},
abortController,
);
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function deleteDeadHost(id: number): Promise<boolean> { export async function deleteDeadHost(id: number, abortController?: AbortController): Promise<boolean> {
return await api.del({ return await api.del(
url: `/nginx/dead-hosts/${id}`, {
}); url: `/nginx/dead-hosts/${id}`,
},
abortController,
);
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function deleteProxyHost(id: number): Promise<boolean> { export async function deleteProxyHost(id: number, abortController?: AbortController): Promise<boolean> {
return await api.del({ return await api.del(
url: `/nginx/proxy-hosts/${id}`, {
}); url: `/nginx/proxy-hosts/${id}`,
},
abortController,
);
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function deleteRedirectionHost(id: number): Promise<boolean> { export async function deleteRedirectionHost(id: number, abortController?: AbortController): Promise<boolean> {
return await api.del({ return await api.del(
url: `/nginx/redirection-hosts/${id}`, {
}); url: `/nginx/redirection-hosts/${id}`,
},
abortController,
);
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function deleteStream(id: number): Promise<boolean> { export async function deleteStream(id: number, abortController?: AbortController): Promise<boolean> {
return await api.del({ return await api.del(
url: `/nginx/streams/${id}`, {
}); url: `/nginx/streams/${id}`,
},
abortController,
);
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function deleteUser(id: number): Promise<boolean> { export async function deleteUser(id: number, abortController?: AbortController): Promise<boolean> {
return await api.del({ return await api.del(
url: `/users/${id}`, {
}); url: `/users/${id}`,
},
abortController,
);
} }

View File

@@ -1,8 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { Binary } from "./responseTypes"; import type { Binary } from "./responseTypes";
export async function downloadCertificate(id: number): Promise<Binary> { export async function downloadCertificate(id: number, abortController?: AbortController): Promise<Binary> {
return await api.get({ return await api.get(
url: `/nginx/certificates/${id}/download`, {
}); url: `/nginx/certificates/${id}/download`,
},
abortController,
);
} }

View File

@@ -1,6 +0,0 @@
export type AccessListExpansion = "owner" | "items" | "clients";
export type AuditLogExpansion = "user";
export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts";
export type HostExpansion = "owner" | "certificate";
export type ProxyHostExpansion = "owner" | "access_list" | "certificate";
export type UserExpansion = "permissions";

View File

@@ -1,13 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { AccessListExpansion } from "./expansions";
import type { AccessList } from "./models"; import type { AccessList } from "./models";
export async function getAccessList(id: number, expand?: AccessListExpansion[], params = {}): Promise<AccessList> { export async function getAccessList(id: number, abortController?: AbortController): Promise<AccessList> {
return await api.get({ return await api.get(
url: `/nginx/access-lists/${id}`, {
params: { url: `/nginx/access-lists/${id}`,
expand: expand?.join(","),
...params,
}, },
}); abortController,
);
} }

View File

@@ -1,7 +1,8 @@
import * as api from "./base"; import * as api from "./base";
import type { AccessListExpansion } from "./expansions";
import type { AccessList } from "./models"; import type { AccessList } from "./models";
export type AccessListExpansion = "owner" | "items" | "clients";
export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise<AccessList[]> { export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise<AccessList[]> {
return await api.get({ return await api.get({
url: "/nginx/access-lists", url: "/nginx/access-lists",

View File

@@ -1,10 +1,9 @@
import * as api from "./base"; import * as api from "./base";
import type { AuditLogExpansion } from "./expansions";
import type { AuditLog } from "./models"; import type { AuditLog } from "./models";
export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise<AuditLog> { export async function getAuditLog(expand?: string[], params = {}): Promise<AuditLog[]> {
return await api.get({ return await api.get({
url: `/audit-log/${id}`, url: "/audit-log",
params: { params: {
expand: expand?.join(","), expand: expand?.join(","),
...params, ...params,

View File

@@ -1,13 +0,0 @@
import * as api from "./base";
import type { AuditLogExpansion } from "./expansions";
import type { AuditLog } from "./models";
export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise<AuditLog[]> {
return await api.get({
url: "/audit-log",
params: {
expand: expand?.join(","),
...params,
},
});
}

View File

@@ -1,13 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { CertificateExpansion } from "./expansions";
import type { Certificate } from "./models"; import type { Certificate } from "./models";
export async function getCertificate(id: number, expand?: CertificateExpansion[], params = {}): Promise<Certificate> { export async function getCertificate(id: number, abortController?: AbortController): Promise<Certificate> {
return await api.get({ return await api.get(
url: `/nginx/certificates/${id}`, {
params: { url: `/nginx/certificates/${id}`,
expand: expand?.join(","),
...params,
}, },
}); abortController,
);
} }

View File

@@ -1,9 +0,0 @@
import * as api from "./base";
import type { DNSProvider } from "./models";
export async function getCertificateDNSProviders(params = {}): Promise<DNSProvider[]> {
return await api.get({
url: "/nginx/certificates/dns-providers",
params,
});
}

View File

@@ -1,8 +1,7 @@
import * as api from "./base"; import * as api from "./base";
import type { CertificateExpansion } from "./expansions";
import type { Certificate } from "./models"; import type { Certificate } from "./models";
export async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise<Certificate[]> { export async function getCertificates(expand?: string[], params = {}): Promise<Certificate[]> {
return await api.get({ return await api.get({
url: "/nginx/certificates", url: "/nginx/certificates",
params: { params: {

View File

@@ -1,13 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { DeadHost } from "./models"; import type { DeadHost } from "./models";
export async function getDeadHost(id: number, expand?: HostExpansion[], params = {}): Promise<DeadHost> { export async function getDeadHost(id: number, abortController?: AbortController): Promise<DeadHost> {
return await api.get({ return await api.get(
url: `/nginx/dead-hosts/${id}`, {
params: { url: `/nginx/dead-hosts/${id}`,
expand: expand?.join(","),
...params,
}, },
}); abortController,
);
} }

View File

@@ -1,8 +1,9 @@
import * as api from "./base"; import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { DeadHost } from "./models"; import type { DeadHost } from "./models";
export async function getDeadHosts(expand?: HostExpansion[], params = {}): Promise<DeadHost[]> { export type DeadHostExpansion = "owner" | "certificate";
export async function getDeadHosts(expand?: DeadHostExpansion[], params = {}): Promise<DeadHost[]> {
return await api.get({ return await api.get({
url: "/nginx/dead-hosts", url: "/nginx/dead-hosts",
params: { params: {

View File

@@ -1,8 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { HealthResponse } from "./responseTypes"; import type { HealthResponse } from "./responseTypes";
export async function getHealth(): Promise<HealthResponse> { export async function getHealth(abortController?: AbortController): Promise<HealthResponse> {
return await api.get({ return await api.get(
url: "/", {
}); url: "/",
},
abortController,
);
} }

View File

@@ -1,7 +1,10 @@
import * as api from "./base"; import * as api from "./base";
export async function getHostsReport(): Promise<Record<string, number>> { export async function getHostsReport(abortController?: AbortController): Promise<Record<string, number>> {
return await api.get({ return await api.get(
url: "/reports/hosts", {
}); url: "/reports/hosts",
},
abortController,
);
} }

View File

@@ -1,13 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { ProxyHostExpansion } from "./expansions";
import type { ProxyHost } from "./models"; import type { ProxyHost } from "./models";
export async function getProxyHost(id: number, expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost> { export async function getProxyHost(id: number, abortController?: AbortController): Promise<ProxyHost> {
return await api.get({ return await api.get(
url: `/nginx/proxy-hosts/${id}`, {
params: { url: `/nginx/proxy-hosts/${id}`,
expand: expand?.join(","),
...params,
}, },
}); abortController,
);
} }

View File

@@ -1,7 +1,8 @@
import * as api from "./base"; import * as api from "./base";
import type { ProxyHostExpansion } from "./expansions";
import type { ProxyHost } from "./models"; import type { ProxyHost } from "./models";
export type ProxyHostExpansion = "owner" | "access_list" | "certificate";
export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost[]> { export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost[]> {
return await api.get({ return await api.get({
url: "/nginx/proxy-hosts", url: "/nginx/proxy-hosts",

View File

@@ -1,13 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { HostExpansion } from "./expansions"; import type { ProxyHost } from "./models";
import type { RedirectionHost } from "./models";
export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise<RedirectionHost> { export async function getRedirectionHost(id: number, abortController?: AbortController): Promise<ProxyHost> {
return await api.get({ return await api.get(
url: `/nginx/redirection-hosts/${id}`, {
params: { url: `/nginx/redirection-hosts/${id}`,
expand: expand?.join(","),
...params,
}, },
}); abortController,
);
} }

View File

@@ -1,8 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { RedirectionHost } from "./models"; import type { RedirectionHost } from "./models";
export async function getRedirectionHosts(expand?: HostExpansion[], params = {}): Promise<RedirectionHost[]> { export type RedirectionHostExpansion = "owner" | "certificate";
export async function getRedirectionHosts(
expand?: RedirectionHostExpansion[],
params = {},
): Promise<RedirectionHost[]> {
return await api.get({ return await api.get({
url: "/nginx/redirection-hosts", url: "/nginx/redirection-hosts",
params: { params: {

View File

@@ -1,12 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { Setting } from "./models"; import type { Setting } from "./models";
export async function getSetting(id: string, expand?: string[], params = {}): Promise<Setting> { export async function getSetting(id: string, abortController?: AbortController): Promise<Setting> {
return await api.get({ return await api.get(
url: `/settings/${id}`, {
params: { url: `/settings/${id}`,
expand: expand?.join(","),
...params,
}, },
}); abortController,
);
} }

View File

@@ -1,13 +1,11 @@
import * as api from "./base"; import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { Stream } from "./models"; import type { Stream } from "./models";
export async function getStream(id: number, expand?: HostExpansion[], params = {}): Promise<Stream> { export async function getStream(id: number, abortController?: AbortController): Promise<Stream> {
return await api.get({ return await api.get(
url: `/nginx/streams/${id}`, {
params: { url: `/nginx/streams/${id}`,
expand: expand?.join(","),
...params,
}, },
}); abortController,
);
} }

View File

@@ -1,8 +1,9 @@
import * as api from "./base"; import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { Stream } from "./models"; import type { Stream } from "./models";
export async function getStreams(expand?: HostExpansion[], params = {}): Promise<Stream[]> { export type StreamExpansion = "owner" | "certificate";
export async function getStreams(expand?: StreamExpansion[], params = {}): Promise<Stream[]> {
return await api.get({ return await api.get({
url: "/nginx/streams", url: "/nginx/streams",
params: { params: {

View File

@@ -1,9 +1,19 @@
import * as api from "./base"; import * as api from "./base";
import type { TokenResponse } from "./responseTypes"; import type { TokenResponse } from "./responseTypes";
export async function getToken(identity: string, secret: string): Promise<TokenResponse> { interface Options {
return await api.post({ payload: {
url: "/tokens", identity: string;
data: { identity, secret }, secret: string;
}); };
}
export async function getToken({ payload }: Options, abortController?: AbortController): Promise<TokenResponse> {
return await api.post(
{
url: "/tokens",
data: payload,
},
abortController,
);
} }

Some files were not shown because too many files have changed in this diff Show More