Compare commits

..

13 Commits

Author SHA1 Message Date
jc21
fdb22e467b Merge pull request #3952 from Trozz/openidc
Merge develop in to openidc
2024-10-11 14:12:20 +10:00
Michael Leer
694d8a0f21 Merge branch 'develop' into openidc 2024-08-22 23:19:50 +01:00
Jamie Curnow
a91dcb144d Use model for db defaults as sqlite doesn't support them 2021-09-09 08:12:50 +10:00
Subv
e7f7be2a2b OpenIDC: Trigger the change event of the "restrict users" toggle when enabling/disabling oidc.
If this is not triggered and the OIDC toggle is enabled, the "disabled" property will be removed from the restricted user list input, causing an error when trying to submit the form without it.
2021-09-09 08:12:50 +10:00
Subv
076d89b5b5 Use localized strings for the OpenID Connect texts. 2021-09-09 08:12:50 +10:00
Subv
8539930f89 Updated the docs to add a section about OpenID Connect 2021-09-09 08:12:50 +10:00
Subv
87d9babbd3 Fix conditionals in the liquid template for OpenID Connect conf. 2021-09-09 08:12:50 +10:00
Subv
9f2d3a1737 Manually set the default values for the OpenID Connect columns.
There is a Knex issue ( https://github.com/knex/knex/issues/2649 ) that prevents .defaultTo from working for text columns.
2021-09-09 08:12:50 +10:00
Subv
daf399163c Allow limiting OpenID Connect auth to a list of users. 2021-09-09 08:12:50 +10:00
Subv
cdf702e545 Add a field to specify a list of allowed emails when using OpenID Connect auth. 2021-09-09 08:12:50 +10:00
Subv
5811345050 Use OpenResty instead of plain nginx to support OpenID Connect authorization. 2021-09-09 08:12:48 +10:00
Subv
53792a5cf7 Add database columns to store OpenID Connect information for Proxy Hosts. 2021-09-09 08:12:19 +10:00
Subv
8e10b7da37 Add UI tab for specifying OpenID Connect options for proxy hosts. 2021-09-09 08:12:19 +10:00
637 changed files with 30922 additions and 25303 deletions

View File

@@ -1 +1 @@
2.12.6 2.11.3

84
Jenkinsfile vendored
View File

@@ -43,7 +43,7 @@ pipeline {
steps { steps {
script { script {
// Defaults to the Branch name, which is applies to all branches AND pr's // Defaults to the Branch name, which is applies to all branches AND pr's
buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}" buildxPushTags = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}"
} }
} }
} }
@@ -56,13 +56,6 @@ pipeline {
sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md' sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md'
} }
} }
stage('Docker Login') {
steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh 'docker login -u "${duser}" -p "${dpass}"'
}
}
}
} }
} }
stage('Builds') { stage('Builds') {
@@ -127,11 +120,6 @@ pipeline {
junit 'test/results/junit/*' junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
} }
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
} }
} }
stage('Test Mysql') { stage('Test Mysql') {
@@ -160,49 +148,6 @@ pipeline {
junit 'test/results/junit/*' junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
} }
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Postgres') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/postgres'
sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
} }
} }
stage('MultiArch Build') { stage('MultiArch Build') {
@@ -212,7 +157,10 @@ pipeline {
} }
} }
steps { steps {
sh "./scripts/buildx --push ${buildxPushTags}" withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh 'docker login -u "${duser}" -p "${dpass}"'
sh "./scripts/buildx --push ${buildxPushTags}"
}
} }
} }
stage('Docs / Comment') { stage('Docs / Comment') {
@@ -241,18 +189,7 @@ pipeline {
} }
steps { steps {
script { script {
npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev): npmGithubPrComment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.", true)
```
nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}
```
> [!NOTE]
> Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
> This is a different docker image namespace than the official image.
> [!WARNING]
> Changes and additions to DNS Providers require verification by at least 2 members of the community!
""", true)
} }
} }
} }
@@ -263,13 +200,20 @@ nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}
always { always {
sh 'echo Reverting ownership' sh 'echo Reverting ownership'
sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data' sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data'
printResult(true) }
success {
juxtapose event: 'success'
sh 'figlet "SUCCESS"'
} }
failure { failure {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
juxtapose event: 'failure'
sh 'figlet "FAILURE"'
} }
unstable { unstable {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
juxtapose event: 'unstable'
sh 'figlet "UNSTABLE"'
} }
} }
} }

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.12.6-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.11.3-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>

73
backend/.eslintrc.json Normal file
View File

@@ -0,0 +1,73 @@
{
"env": {
"node": true,
"es6": true
},
"extends": [
"eslint:recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"align-assignments"
],
"rules": {
"arrow-parens": [
"error",
"always"
],
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"key-spacing": [
"error",
{
"align": "value"
}
],
"comma-spacing": [
"error",
{
"before": false,
"after": true
}
],
"func-call-spacing": [
"error",
"never"
],
"keyword-spacing": [
"error",
{
"before": true
}
],
"no-irregular-whitespace": "error",
"no-unused-expressions": 0,
"align-assignments/align-assignments": [
2,
{
"requiresOnly": false
}
]
}
}

11
backend/.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"printWidth": 320,
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"jsxBracketSameLine": true,
"trailingComma": "all",
"proseWrap": "always"
}

8
backend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"editor.insertSpaces": false,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

View File

@@ -1,12 +1,9 @@
import bodyParser from "body-parser"; const express = require('express');
import compression from "compression"; const bodyParser = require('body-parser');
import express from "express"; const fileUpload = require('express-fileupload');
import fileUpload from "express-fileupload"; const compression = require('compression');
import { isDebugMode } from "./lib/config.js"; const config = require('./lib/config');
import cors from "./lib/express/cors.js"; const log = require('./logger').express;
import jwt from "./lib/express/jwt.js";
import { express as logger } from "./logger.js";
import mainRoutes from "./routes/main.js";
/** /**
* App * App
@@ -14,7 +11,7 @@ import mainRoutes from "./routes/main.js";
const app = express(); const app = express();
app.use(fileUpload()); app.use(fileUpload());
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({extended: true}));
// Gzip // Gzip
app.use(compression()); app.use(compression());
@@ -23,70 +20,71 @@ app.use(compression());
* General Logging, BEFORE routes * General Logging, BEFORE routes
*/ */
app.disable("x-powered-by"); app.disable('x-powered-by');
app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]); app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.enable("strict routing"); app.enable('strict routing');
// pretty print JSON when not live // pretty print JSON when not live
if (isDebugMode()) { if (config.debug()) {
app.set("json spaces", 2); app.set('json spaces', 2);
} }
// CORS for everything // CORS for everything
app.use(cors); app.use(require('./lib/express/cors'));
// General security/cache related headers + server header // General security/cache related headers + server header
app.use((_, res, next) => { app.use(function (req, res, next) {
let x_frame_options = "DENY"; let x_frame_options = 'DENY';
if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) { if (typeof process.env.X_FRAME_OPTIONS !== 'undefined' && process.env.X_FRAME_OPTIONS) {
x_frame_options = process.env.X_FRAME_OPTIONS; x_frame_options = process.env.X_FRAME_OPTIONS;
} }
res.set({ res.set({
"X-XSS-Protection": "1; mode=block", 'X-XSS-Protection': '1; mode=block',
"X-Content-Type-Options": "nosniff", 'X-Content-Type-Options': 'nosniff',
"X-Frame-Options": x_frame_options, 'X-Frame-Options': x_frame_options,
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate", 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
Pragma: "no-cache", Pragma: 'no-cache',
Expires: 0, Expires: 0
}); });
next(); next();
}); });
app.use(jwt()); app.use(require('./lib/express/jwt')());
app.use("/", mainRoutes); app.use('/', require('./routes/api/main'));
// production error handler // production error handler
// no stacktraces leaked to user // no stacktraces leaked to user
app.use((err, req, res, _) => { // eslint-disable-next-line
const payload = { app.use(function (err, req, res, next) {
let payload = {
error: { error: {
code: err.status, code: err.status,
message: err.public ? err.message : "Internal Error", message: err.public ? err.message : 'Internal Error'
}, }
}; };
if (typeof err.message_i18n !== "undefined") { if (config.debug() || (req.baseUrl + req.path).includes('nginx/certificates')) {
payload.error.message_i18n = err.message_i18n;
}
if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
payload.debug = { payload.debug = {
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null, stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
previous: err.previous, previous: err.previous
}; };
} }
// Not every error is worth logging - but this is good for now until it gets annoying. // Not every error is worth logging - but this is good for now until it gets annoying.
if (typeof err.stack !== "undefined" && err.stack) { if (typeof err.stack !== 'undefined' && err.stack) {
logger.debug(err.stack); if (config.debug()) {
if (typeof err.public === "undefined" || !err.public) { log.debug(err.stack);
logger.warn(err.message); } else if (typeof err.public == 'undefined' || !err.public) {
log.warn(err.message);
} }
} }
res.status(err.status || 500).send(payload); res
.status(err.status || 500)
.send(payload);
}); });
export default app; module.exports = app;

View File

@@ -1,91 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"!**/dist/**/*"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 4,
"lineWidth": 120,
"formatWithErrors": true
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":BUN:",
":NODE:",
[
"npm:*",
"npm:*/**"
],
":PACKAGE_WITH_PROTOCOL:",
":URL:",
":PACKAGE:",
[
"/src/*",
"/src/**"
],
[
"/**"
],
[
"#*",
"#*/**"
],
":PATH:"
]
}
}
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useUniqueElementIds": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"performance": {
"noDelete": "off"
},
"nursery": "off",
"a11y": {
"useSemanticElements": "off",
"useValidAnchor": "off"
},
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
}
}

View File

@@ -1,6 +1,6 @@
{ {
"database": { "database": {
"engine": "mysql2", "engine": "mysql",
"host": "db", "host": "db",
"name": "npm", "name": "npm",
"user": "npm", "user": "npm",

View File

@@ -1,32 +1,27 @@
import knex from "knex"; const config = require('./lib/config');
import {configGet, configHas} from "./lib/config.js";
const generateDbConfig = () => { if (!config.has('database')) {
if (!configHas("database")) { throw new Error('Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/');
throw new Error( }
"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/",
);
}
const cfg = configGet("database"); function generateDbConfig() {
const cfg = config.get('database');
if (cfg.engine === "knex-native") { if (cfg.engine === 'knex-native') {
return cfg.knex; return cfg.knex;
} }
return { return {
client: cfg.engine, client: cfg.engine,
connection: { connection: {
host: cfg.host, host: cfg.host,
user: cfg.user, user: cfg.user,
password: cfg.password, password: cfg.password,
database: cfg.name, database: cfg.name,
port: cfg.port, port: cfg.port
}, },
migrations: { migrations: {
tableName: "migrations", tableName: 'migrations'
}, }
}; };
}; }
export default knex(generateDbConfig()); module.exports = require('knex')(generateDbConfig());

1456
backend/doc/api.swagger.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,40 @@
#!/usr/bin/env node #!/usr/bin/env node
import app from "./app.js"; const logger = require('./logger').global;
import internalCertificate from "./internal/certificate.js";
import internalIpRanges from "./internal/ip_ranges.js";
import { global as logger } from "./logger.js";
import { migrateUp } from "./migrate.js";
import { getCompiledSchema } from "./schema/index.js";
import setup from "./setup.js";
const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== "false"; async function appStart () {
const migrate = require('./migrate');
const setup = require('./setup');
const app = require('./app');
const apiValidator = require('./lib/validator/api');
const internalCertificate = require('./internal/certificate');
const internalIpRanges = require('./internal/ip_ranges');
async function appStart() { return migrate.latest()
return migrateUp()
.then(setup) .then(setup)
.then(getCompiledSchema)
.then(() => { .then(() => {
if (!IP_RANGES_FETCH_ENABLED) { return apiValidator.loadSchemas;
logger.info("IP Ranges fetch is disabled by environment variable");
return;
}
logger.info("IP Ranges fetch is enabled");
return internalIpRanges.fetch().catch((err) => {
logger.error("IP Ranges fetch failed, continuing anyway:", err.message);
});
}) })
.then(internalIpRanges.fetch)
.then(() => { .then(() => {
internalCertificate.initTimer(); internalCertificate.initTimer();
internalIpRanges.initTimer(); internalIpRanges.initTimer();
const server = app.listen(3000, () => { const server = app.listen(3000, () => {
logger.info(`Backend PID ${process.pid} listening on port 3000 ...`); logger.info('Backend PID ' + process.pid + ' listening on port 3000 ...');
process.on("SIGTERM", () => { process.on('SIGTERM', () => {
logger.info(`PID ${process.pid} received SIGTERM`); logger.info('PID ' + process.pid + ' received SIGTERM');
server.close(() => { server.close(() => {
logger.info("Stopping."); logger.info('Stopping.');
process.exit(0); process.exit(0);
}); });
}); });
}); });
}) })
.catch((err) => { .catch((err) => {
logger.error(`Startup Error: ${err.message}`, err); logger.error(err.message);
setTimeout(appStart, 1000); setTimeout(appStart, 1000);
}); });
} }
@@ -49,6 +42,7 @@ async function appStart() {
try { try {
appStart(); appStart();
} catch (err) { } catch (err) {
logger.fatal(err); logger.error(err.message, err);
process.exit(1); process.exit(1);
} }

View File

@@ -1,94 +1,103 @@
import fs from "node:fs"; const _ = require('lodash');
import batchflow from "batchflow"; const fs = require('fs');
import _ from "lodash"; const batchflow = require('batchflow');
import errs from "../lib/error.js"; const logger = require('../logger').access;
import utils from "../lib/utils.js"; const error = require('../lib/error');
import { access as logger } from "../logger.js"; const utils = require('../lib/utils');
import accessListModel from "../models/access_list.js"; const accessListModel = require('../models/access_list');
import accessListAuthModel from "../models/access_list_auth.js"; const accessListAuthModel = require('../models/access_list_auth');
import accessListClientModel from "../models/access_list_client.js"; const accessListClientModel = require('../models/access_list_client');
import proxyHostModel from "../models/proxy_host.js"; const proxyHostModel = require('../models/proxy_host');
import internalAuditLog from "./audit-log.js"; const internalAuditLog = require('./audit-log');
import internalNginx from "./nginx.js"; const internalNginx = require('./nginx');
const omissions = () => { function omissions () {
return ["is_deleted"]; return ['is_deleted'];
}; }
const internalAccessList = { const internalAccessList = {
/** /**
* @param {Access} access * @param {Access} access
* @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.can('access_lists:create', data)
const row = await accessListModel .then((/*access_data*/) => {
.query() return accessListModel
.insertAndFetch({ .query()
name: data.name, .insertAndFetch({
satisfy_any: data.satisfy_any, name: data.name,
pass_auth: data.pass_auth, satisfy_any: data.satisfy_any,
owner_user_id: access.token.getUserId(1), 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; let promises = [];
const promises = []; // Now add the items
// Items data.items.map((item) => {
data.items.map((item) => { promises.push(accessListAuthModel
promises.push( .query()
accessListAuthModel.query().insert({ .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; });
});
// 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(accessListClientModel
access_list_id: row.id, .query()
address: client.address, .insert({
directive: client.directive, access_list_id: row.id,
}), address: client.address,
); directive: client.directive
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.build(row)
const freshRow = await internalAccessList.get( .then(() => {
access, if (row.proxy_host_count) {
{ return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
id: data.id, }
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"], })
}, .then(() => {
true // skip masking // Add to audit log
); return internalAuditLog.add(access, {
action: 'created',
// Audit log object_type: 'access-list',
data.meta = _.assign({}, data.meta || {}, freshRow.meta); object_id: row.id,
await internalAccessList.build(freshRow); 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 +108,129 @@ 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.can('access_lists:update', data.id)
const row = await internalAccessList.get(access, { id: data.id }); .then((/*access_data*/) => {
if (row.id !== data.id) { return internalAccessList.get(access, {id: data.id});
// Sanity check that something crazy hasn't happened })
throw new errs.InternalValidationError( .then((row) => {
`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`, if (row.id !== data.id) {
); // Sanity check that something crazy hasn't happened
} throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
// patch name if specified
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; })
}); .then(() => {
// patch name if specified
const query = accessListAuthModel.query().delete().where("access_list_id", data.id); if (typeof data.name !== 'undefined' && data.name) {
return accessListModel
if (itemsToKeep.length) { .query()
query.andWhere("username", "NOT IN", itemsToKeep); .where({id: data.id})
} .patch({
name: data.name,
await query; satisfy_any: data.satisfy_any,
// Add new items pass_auth: data.pass_auth,
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(() => {
// Check for items and add/update/remove them
if (typeof data.items !== 'undefined' && data.items) {
let promises = [];
let items_to_keep = [];
data.items.map(function (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);
}
});
let 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) {
let promises = [];
data.clients.map(function (client) {
if (client.address) {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: data.id,
address: client.address,
directive: client.directive
})
);
}
});
let 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 (row.proxy_host_count) {
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 +239,48 @@ 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 || {}; if (typeof data === 'undefined') {
const accessData = await access.can("access_lists:get", thisData.id) data = {};
}
const query = accessListModel return access.can('access_lists:get', data.id)
.query() .then((access_data) => {
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count")) let query = accessListModel
.leftJoin("proxy_host", function () { .query()
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn( .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
"proxy_host.is_deleted", .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
"=", .where('access_list.is_deleted', 0)
0, .andWhere('access_list.id', data.id)
); .allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
}) })
.where("access_list.is_deleted", 0) .then((row) => {
.andWhere("access_list.id", thisData.id) if (!row) {
.groupBy("access_list.id") throw new error.ItemNotFoundError(data.id);
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]") }
.first(); if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
if (accessData.permission_visibility !== "all") { }
query.andWhere("access_list.owner_user_id", access.token.getUserId(1)); // Custom omissions
} if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { }
query.withGraphFetched(`[${thisData.expand.join(", ")}]`); return row;
} });
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 +290,73 @@ 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.can('access_lists:delete', data.id)
const row = await internalAccessList.get(access, { .then(() => {
id: data.id, return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
expand: ["proxy_hosts", "items", "clients"], })
}); .then((row) => {
if (!row) {
throw new error.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(function (val, idx) {
.query() row.proxy_hosts[idx].access_list_id = 0;
.where("id", row.id) });
.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
let 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 +364,72 @@ 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) => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `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(function (row, idx) {
if (typeof row.items !== 'undefined' && row.items) {
rows[idx] = internalAccessList.maskItems(row);
}
});
}
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 let query = accessListModel
.query() .query()
.count("id as count") .count('id as count')
.where("is_deleted", 0); .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()
return Number.parseInt(row.count, 10); .then((row) => {
return parseInt(row.count, 10);
});
}, },
/** /**
@@ -400,21 +437,21 @@ const internalAccessList = {
* @returns {Object} * @returns {Object}
*/ */
maskItems: (list) => { maskItems: (list) => {
if (list && typeof list.items !== "undefined") { if (list && typeof list.items !== 'undefined') {
list.items.map((val, idx) => { list.items.map(function (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 list; return list;
}, },
@@ -424,7 +461,7 @@ const internalAccessList = {
* @returns {String} * @returns {String}
*/ */
getFilename: (list) => { getFilename: (list) => {
return `/data/access/${list.id}`; return '/data/access/' + list.id;
}, },
/** /**
@@ -434,55 +471,58 @@ 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) => {
let 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.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
if (list.items.length) { .then((/*result*/) => {
await new Promise((resolve, reject) => { next();
batchflow(list.items).sequential() })
.each((_i, item, next) => { .catch((err) => {
if (item.password?.length) { logger.error(err);
logger.info(`Adding: ${item.username}`); next(err);
});
utils.execFile('openssl', ['passwd', '-apr1', item.password]) }
.then((res) => { })
try { .error((err) => {
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'}); logger.error(err);
} catch (err) { reject(err);
reject(err); })
} .end((results) => {
next(); logger.success('Built Access file #' + list.id + ' for: ' + list.name);
}) resolve(results);
.catch((err) => { });
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success(`Built Access file #${list.id} for: ${list.name}`);
resolve(results);
}); });
}
}); });
}
} }
} };
export default internalAccessList; module.exports = internalAccessList;

View File

@@ -1,6 +1,5 @@
import errs from "../lib/error.js"; const error = require('../lib/error');
import { castJsonIfNeed } from "../lib/helpers.js"; const auditLogModel = require('../models/audit-log');
import auditLogModel from "../models/audit-log.js";
const internalAuditLog = { const internalAuditLog = {
@@ -9,31 +8,32 @@ 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(() => {
let 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') {
.orderBy("created_on", "DESC") query.where(function () {
.orderBy("id", "DESC") this.where('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}`);
return query;
}); });
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return await query;
}, },
/** /**
@@ -50,24 +50,29 @@ 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 error.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(auditLogModel
return await auditLogModel.query().insert({ .query()
user_id: data.user_id, .insert({
action: data.action, user_id: data.user_id,
object_type: data.object_type || "", action: data.action,
object_id: data.object_id || 0, object_type: data.object_type || '',
meta: data.meta || {}, object_id: data.object_id || 0,
meta: data.meta || {}
}));
}
}); });
}, }
}; };
export default internalAuditLog; module.exports = internalAuditLog;

File diff suppressed because it is too large Load Diff

View File

@@ -1,89 +1,103 @@
import _ from "lodash"; const _ = require('lodash');
import errs from "../lib/error.js"; const error = require('../lib/error');
import { castJsonIfNeed } from "../lib/helpers.js"; const utils = require('../lib/utils');
import utils from "../lib/utils.js"; const deadHostModel = require('../models/dead_host');
import deadHostModel from "../models/dead_host.js"; const internalHost = require('./host');
import internalAuditLog from "./audit-log.js"; const internalNginx = require('./nginx');
import internalCertificate from "./certificate.js"; const internalAuditLog = require('./audit-log');
import internalHost from "./host.js"; const internalCertificate = require('./certificate');
import internalNginx from "./nginx.js";
const omissions = () => { function omissions () {
return ["is_deleted"]; return ['is_deleted'];
}; }
const internalDeadHost = { const internalDeadHost = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @returns {Promise} * @returns {Promise}
*/ */
create: async (access, data) => { create: (access, data) => {
const createCertificate = data.certificate_id === "new"; let create_certificate = data.certificate_id === 'new';
if (createCertificate) { if (create_certificate) {
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
let domain_name_check_promises = [];
// Get a list of the domain names and check each of them against existing records data.domain_names.map(function (domain_name) {
const domainNameCheckPromises = []; domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
});
data.domain_names.map((domain_name) => { return Promise.all(domain_name_check_promises)
domainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name)); .then((check_results) => {
return true; check_results.map(function (result) {
}); if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
})
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
await Promise.all(domainNameCheckPromises).then((check_results) => { return deadHostModel
check_results.map((result) => { .query()
if (result.is_taken) { .insertAndFetch(data)
throw new errs.ValidationError(`${result.hostname} is already in use`); .then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
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;
});
} else {
return row;
} }
return true; })
.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()));
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"],
});
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", freshRow);
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "dead-host",
object_id: freshRow.id,
meta: data,
});
return freshRow;
}, },
/** /**
@@ -92,79 +106,98 @@ 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 create_certificate = data.certificate_id === 'new';
if (createCertificate) { if (create_certificate) {
delete data.certificate_id; delete data.certificate_id;
} }
await access.can("dead_hosts:update", data.id); return access.can('dead_hosts:update', data.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
// Get a list of the domain names and check each of them against existing records if (typeof data.domain_names !== 'undefined') {
const domainNameCheckPromises = []; data.domain_names.map(function (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) => { });
domainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, "dead", data.id));
return true;
});
const checkResults = await Promise.all(domainNameCheckPromises); return Promise.all(domain_name_check_promises)
checkResults.map((result) => { .then((check_results) => {
if (result.is_taken) { check_results.map(function (result) {
throw new errs.ValidationError(`${result.hostname} is already in use`); if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
} }
return true; })
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then((cert) => {
// update host with cert id
data.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
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.
data = _.assign({}, {
domain_names: row.domain_names
}, data);
data = internalHost.cleanSslHstsData(data, row);
return deadHostModel
.query()
.where({id: data.id})
.patch(data)
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalDeadHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
})
.then((row) => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(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);
// 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());
}, },
/** /**
@@ -175,32 +208,40 @@ 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); if (typeof data === 'undefined') {
const query = deadHostModel data = {};
.query()
.where("is_deleted", 0)
.andWhere("id", data.id)
.allowGraph("[owner,certificate]")
.first();
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
} }
if (typeof data.expand !== "undefined" && data.expand !== null) { return access.can('dead_hosts:get', data.id)
query.withGraphFetched(`[${data.expand.join(", ")}]`); .then((access_data) => {
} let query = deadHostModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner,certificate]')
.first();
const row = await query.then(utils.omitRow(omissions())); if (access_data.permission_visibility !== 'all') {
if (!row || !row.id) { query.andWhere('owner_user_id', access.token.getUserId(1));
throw new errs.ItemNotFoundError(data.id); }
}
// Custom omissions if (typeof data.expand !== 'undefined' && data.expand !== null) {
if (typeof data.omit !== "undefined" && data.omit !== null) { query.withGraphFetched('[' + data.expand.join(', ') + ']');
return _.omit(row, data.omit); }
}
return row; return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
}
return row;
});
}, },
/** /**
@@ -210,30 +251,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.can('dead_hosts:delete', data.id)
const row = await internalDeadHost.get(access, { id: data.id }); .then(() => {
if (!row || !row.id) { return internalDeadHost.get(access, {id: data.id});
throw new errs.ItemNotFoundError(data.id); })
} .then((row) => {
if (!row) {
throw new error.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()),
});
}, },
/** /**
@@ -243,39 +296,46 @@ 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.can('dead_hosts:update', data.id)
const row = await internalDeadHost.get(access, { .then(() => {
id: data.id, return internalDeadHost.get(access, {
expand: ["certificate", "owner"], id: data.id,
}); expand: ['certificate', 'owner']
if (!row || !row.id) { });
throw new errs.ItemNotFoundError(data.id); })
} .then((row) => {
if (row.enabled) { if (!row) {
throw new errs.ValidationError("Host is already enabled"); throw new error.ItemNotFoundError(data.id);
} } else if (row.enabled) {
throw new error.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;
}, },
/** /**
@@ -285,37 +345,46 @@ 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.can('dead_hosts:update', data.id)
const row = await internalDeadHost.get(access, { id: data.id }); .then(() => {
if (!row || !row.id) { return internalDeadHost.get(access, {id: data.id});
throw new errs.ItemNotFoundError(data.id); })
} .then((row) => {
if (!row.enabled) { if (!row) {
throw new errs.ValidationError("Host is already disabled"); throw new error.ItemNotFoundError(data.id);
} } else if (!row.enabled) {
throw new error.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;
}, },
/** /**
@@ -323,38 +392,43 @@ 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.can('dead_hosts:list')
const query = deadHostModel .then((access_data) => {
.query() let query = deadHostModel
.where("is_deleted", 0) .query()
.groupBy("id") .where('is_deleted', 0)
.allowGraph("[owner,certificate]") .groupBy('id')
.orderBy(castJsonIfNeed("domain_names"), "ASC"); .allowGraph('[owner,certificate]')
.orderBy('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') {
query.where(function () { query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`); this.where('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;
}, },
/** /**
@@ -364,16 +438,21 @@ 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); let 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()
return Number.parseInt(row.count, 10); .then((row) => {
}, return parseInt(row.count, 10);
});
}
}; };
export default internalDeadHost; module.exports = internalDeadHost;

View File

@@ -1,10 +1,10 @@
import _ from "lodash"; const _ = require('lodash');
import { castJsonIfNeed } from "../lib/helpers.js"; const proxyHostModel = require('../models/proxy_host');
import deadHostModel from "../models/dead_host.js"; const redirectionHostModel = require('../models/redirection_host');
import proxyHostModel from "../models/proxy_host.js"; const deadHostModel = require('../models/dead_host');
import redirectionHostModel from "../models/redirection_host.js";
const internalHost = { const internalHost = {
/** /**
* Makes sure that the ssl_* and hsts_* fields play nicely together. * Makes sure that the ssl_* and hsts_* fields play nicely together.
* ie: if there is no cert, then force_ssl is off. * ie: if there is no cert, then force_ssl is off.
@@ -14,23 +14,25 @@ const internalHost = {
* @param {object} [existing_data] * @param {object} [existing_data]
* @returns {object} * @returns {object}
*/ */
cleanSslHstsData: (data, existingData) => { cleanSslHstsData: function (data, existing_data) {
const combinedData = _.assign({}, existingData || {}, data); existing_data = existing_data === undefined ? {} : existing_data;
if (!combinedData.certificate_id) { let combined_data = _.assign({}, existing_data, data);
combinedData.ssl_forced = false;
combinedData.http2_support = false; if (!combined_data.certificate_id) {
combined_data.ssl_forced = false;
combined_data.http2_support = false;
} }
if (!combinedData.ssl_forced) { if (!combined_data.ssl_forced) {
combinedData.hsts_enabled = false; combined_data.hsts_enabled = false;
} }
if (!combinedData.hsts_enabled) { if (!combined_data.hsts_enabled) {
combinedData.hsts_subdomains = false; combined_data.hsts_subdomains = false;
} }
return combinedData; return combined_data;
}, },
/** /**
@@ -39,12 +41,11 @@ const internalHost = {
* @param {Array} rows * @param {Array} rows
* @returns {Array} * @returns {Array}
*/ */
cleanAllRowsCertificateMeta: (rows) => { cleanAllRowsCertificateMeta: function (rows) {
rows.map((_, idx) => { rows.map(function (row, idx) {
if (typeof rows[idx].certificate !== "undefined" && rows[idx].certificate) { if (typeof rows[idx].certificate !== 'undefined' && rows[idx].certificate) {
rows[idx].certificate.meta = {}; rows[idx].certificate.meta = {};
} }
return true;
}); });
return rows; return rows;
@@ -56,8 +57,8 @@ const internalHost = {
* @param {Object} row * @param {Object} row
* @returns {Object} * @returns {Object}
*/ */
cleanRowCertificateMeta: (row) => { cleanRowCertificateMeta: function (row) {
if (typeof row.certificate !== "undefined" && row.certificate) { if (typeof row.certificate !== 'undefined' && row.certificate) {
row.certificate.meta = {}; row.certificate.meta = {};
} }
@@ -65,33 +66,54 @@ 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: function (domain_names) {
const responseObject = { let promises = [
total_count: 0, proxyHostModel
dead_hosts: [], .query()
proxy_hosts: [], .where('is_deleted', 0),
redirection_hosts: [], redirectionHostModel
}; .query()
.where('is_deleted', 0),
deadHostModel
.query()
.where('is_deleted', 0)
];
const proxyRes = await proxyHostModel.query().where("is_deleted", 0); return Promise.all(promises)
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames); .then((promises_results) => {
responseObject.total_count += responseObject.proxy_hosts.length; let response_object = {
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;
});
}, },
/** /**
@@ -102,133 +124,112 @@ const internalHost = {
* @param {Integer} [ignore_id] Must be supplied if type was also supplied * @param {Integer} [ignore_id] Must be supplied if type was also supplied
* @returns {Promise} * @returns {Promise}
*/ */
isHostnameTaken: (hostname, ignore_type, ignore_id) => { isHostnameTaken: function (hostname, ignore_type, ignore_id) {
const promises = [ let promises = [
proxyHostModel proxyHostModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), .andWhere('domain_names', 'like', '%' + hostname + '%'),
redirectionHostModel redirectionHostModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), .andWhere('domain_names', 'like', '%' + hostname + '%'),
deadHostModel deadHostModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), .andWhere('domain_names', 'like', '%' + hostname + '%')
]; ];
return Promise.all(promises).then((promises_results) => { return Promise.all(promises)
let is_taken = false; .then((promises_results) => {
let is_taken = false;
if (promises_results[0]) { if (promises_results[0]) {
// Proxy Hosts // Proxy Hosts
if ( if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[0], ignore_type === 'proxy' && ignore_id ? ignore_id : 0)) {
internalHost._checkHostnameRecordsTaken( is_taken = true;
hostname, }
promises_results[0],
ignore_type === "proxy" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
} }
}
if (promises_results[1]) { if (promises_results[1]) {
// Redirection Hosts // Redirection Hosts
if ( if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[1], ignore_type === 'redirection' && ignore_id ? ignore_id : 0)) {
internalHost._checkHostnameRecordsTaken( is_taken = true;
hostname, }
promises_results[1],
ignore_type === "redirection" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
} }
}
if (promises_results[2]) { if (promises_results[2]) {
// Dead Hosts // Dead Hosts
if ( if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) {
internalHost._checkHostnameRecordsTaken( is_taken = true;
hostname, }
promises_results[2],
ignore_type === "dead" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
} }
}
return { return {
hostname: hostname, hostname: hostname,
is_taken: is_taken, is_taken: is_taken
}; };
}); });
}, },
/** /**
* Private call only * Private call only
* *
* @param {String} hostname * @param {String} hostname
* @param {Array} existingRows * @param {Array} existing_rows
* @param {Integer} [ignoreId] * @param {Integer} [ignore_id]
* @returns {Boolean} * @returns {Boolean}
*/ */
_checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => { _checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) {
let isTaken = false; let is_taken = false;
if (existingRows?.length) { if (existing_rows && existing_rows.length) {
existingRows.map((existingRow) => { existing_rows.map(function (existing_row) {
existingRow.domain_names.map((existingHostname) => { existing_row.domain_names.map(function (existing_hostname) {
// Does this domain match? // Does this domain match?
if (existingHostname.toLowerCase() === hostname.toLowerCase()) { if (existing_hostname.toLowerCase() === hostname.toLowerCase()) {
if (!ignoreId || ignoreId !== existingRow.id) { if (!ignore_id || ignore_id !== existing_row.id) {
isTaken = true; is_taken = true;
} }
} }
return true;
}); });
return true;
}); });
} }
return isTaken; return is_taken;
}, },
/** /**
* Private call only * Private call only
* *
* @param {Array} hosts * @param {Array} hosts
* @param {Array} domainNames * @param {Array} domain_names
* @returns {Array} * @returns {Array}
*/ */
_getHostsWithDomains: (hosts, domainNames) => { _getHostsWithDomains: function (hosts, domain_names) {
const response = []; let response = [];
if (hosts?.length) { if (hosts && hosts.length) {
hosts.map((host) => { hosts.map(function (host) {
let hostMatches = false; let host_matches = false;
domainNames.map((domainName) => { domain_names.map(function (domain_name) {
host.domain_names.map((hostDomainName) => { host.domain_names.map(function (host_domain_name) {
if (domainName.toLowerCase() === hostDomainName.toLowerCase()) { if (domain_name.toLowerCase() === host_domain_name.toLowerCase()) {
hostMatches = true; host_matches = true;
} }
return true;
}); });
return true;
}); });
if (hostMatches) { if (host_matches) {
response.push(host); response.push(host);
} }
return true;
}); });
} }
return response; return response;
}, }
}; };
export default internalHost; module.exports = internalHost;

View File

@@ -1,51 +1,45 @@
import fs from "node:fs"; const https = require('https');
import https from "node:https"; const fs = require('fs');
import { dirname } from "node:path"; const logger = require('../logger').ip_ranges;
import { fileURLToPath } from "node:url"; const error = require('../lib/error');
import errs from "../lib/error.js"; const utils = require('../lib/utils');
import utils from "../lib/utils.js"; const internalNginx = require('./nginx');
import { ipRanges as logger } from "../logger.js";
import internalNginx from "./nginx.js";
const __filename = fileURLToPath(import.meta.url); const CLOUDFRONT_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json';
const __dirname = dirname(__filename); const CLOUDFARE_V4_URL = 'https://www.cloudflare.com/ips-v4';
const CLOUDFARE_V6_URL = 'https://www.cloudflare.com/ips-v6';
const CLOUDFRONT_URL = "https://ip-ranges.amazonaws.com/ip-ranges.json";
const CLOUDFARE_V4_URL = "https://www.cloudflare.com/ips-v4";
const CLOUDFARE_V6_URL = "https://www.cloudflare.com/ips-v6";
const regIpV4 = /^(\d+\.?){4}\/\d+/; const regIpV4 = /^(\d+\.?){4}\/\d+/;
const regIpV6 = /^(([\da-fA-F]+)?:)+\/\d+/; const regIpV6 = /^(([\da-fA-F]+)?:)+\/\d+/;
const internalIpRanges = { const internalIpRanges = {
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null, interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null,
interval_processing: false, interval_processing: false,
iteration_count: 0, iteration_count: 0,
initTimer: () => { initTimer: () => {
logger.info("IP Ranges Renewal Timer initialized"); logger.info('IP Ranges Renewal Timer initialized');
internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout); internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);
}, },
fetchUrl: (url) => { fetchUrl: (url) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.info(`Fetching ${url}`); logger.info('Fetching ' + url);
return https return https.get(url, (res) => {
.get(url, (res) => { res.setEncoding('utf8');
res.setEncoding("utf8"); let raw_data = '';
let raw_data = ""; res.on('data', (chunk) => {
res.on("data", (chunk) => { raw_data += chunk;
raw_data += chunk;
});
res.on("end", () => {
resolve(raw_data);
});
})
.on("error", (err) => {
reject(err);
}); });
res.on('end', () => {
resolve(raw_data);
});
}).on('error', (err) => {
reject(err);
});
}); });
}, },
@@ -55,30 +49,27 @@ const internalIpRanges = {
fetch: () => { fetch: () => {
if (!internalIpRanges.interval_processing) { if (!internalIpRanges.interval_processing) {
internalIpRanges.interval_processing = true; internalIpRanges.interval_processing = true;
logger.info("Fetching IP Ranges from online services..."); logger.info('Fetching IP Ranges from online services...');
let ip_ranges = []; let ip_ranges = [];
return internalIpRanges return internalIpRanges.fetchUrl(CLOUDFRONT_URL)
.fetchUrl(CLOUDFRONT_URL)
.then((cloudfront_data) => { .then((cloudfront_data) => {
const data = JSON.parse(cloudfront_data); let data = JSON.parse(cloudfront_data);
if (data && typeof data.prefixes !== "undefined") { if (data && typeof data.prefixes !== 'undefined') {
data.prefixes.map((item) => { data.prefixes.map((item) => {
if (item.service === "CLOUDFRONT") { if (item.service === 'CLOUDFRONT') {
ip_ranges.push(item.ip_prefix); ip_ranges.push(item.ip_prefix);
} }
return true;
}); });
} }
if (data && typeof data.ipv6_prefixes !== "undefined") { if (data && typeof data.ipv6_prefixes !== 'undefined') {
data.ipv6_prefixes.map((item) => { data.ipv6_prefixes.map((item) => {
if (item.service === "CLOUDFRONT") { if (item.service === 'CLOUDFRONT') {
ip_ranges.push(item.ipv6_prefix); ip_ranges.push(item.ipv6_prefix);
} }
return true;
}); });
} }
}) })
@@ -86,38 +77,38 @@ const internalIpRanges = {
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL); return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
}) })
.then((cloudfare_data) => { .then((cloudfare_data) => {
const items = cloudfare_data.split("\n").filter((line) => regIpV4.test(line)); let items = cloudfare_data.split('\n').filter((line) => regIpV4.test(line));
ip_ranges = [...ip_ranges, ...items]; ip_ranges = [... ip_ranges, ... items];
}) })
.then(() => { .then(() => {
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL); return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
}) })
.then((cloudfare_data) => { .then((cloudfare_data) => {
const items = cloudfare_data.split("\n").filter((line) => regIpV6.test(line)); let items = cloudfare_data.split('\n').filter((line) => regIpV6.test(line));
ip_ranges = [...ip_ranges, ...items]; ip_ranges = [... ip_ranges, ... items];
}) })
.then(() => { .then(() => {
const clean_ip_ranges = []; let clean_ip_ranges = [];
ip_ranges.map((range) => { ip_ranges.map((range) => {
if (range) { if (range) {
clean_ip_ranges.push(range); clean_ip_ranges.push(range);
} }
return true;
}); });
return internalIpRanges.generateConfig(clean_ip_ranges).then(() => { return internalIpRanges.generateConfig(clean_ip_ranges)
if (internalIpRanges.iteration_count) { .then(() => {
// Reload nginx if (internalIpRanges.iteration_count) {
return internalNginx.reload(); // Reload nginx
} return internalNginx.reload();
}); }
});
}) })
.then(() => { .then(() => {
internalIpRanges.interval_processing = false; internalIpRanges.interval_processing = false;
internalIpRanges.iteration_count++; internalIpRanges.iteration_count++;
}) })
.catch((err) => { .catch((err) => {
logger.fatal(err.message); logger.error(err.message);
internalIpRanges.interval_processing = false; internalIpRanges.interval_processing = false;
}); });
} }
@@ -131,26 +122,26 @@ const internalIpRanges = {
const renderEngine = utils.getRenderEngine(); const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
const filename = "/etc/nginx/conf.d/include/ip_ranges.conf"; let filename = '/etc/nginx/conf.d/include/ip_ranges.conf';
try { try {
template = fs.readFileSync(`${__dirname}/../templates/ip_ranges.conf`, { encoding: "utf8" }); template = fs.readFileSync(__dirname + '/../templates/ip_ranges.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new errs.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
} }
renderEngine renderEngine
.parseAndRender(template, { ip_ranges: ip_ranges }) .parseAndRender(template, {ip_ranges: ip_ranges})
.then((config_text) => { .then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" }); fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
resolve(true); resolve(true);
}) })
.catch((err) => { .catch((err) => {
logger.warn(`Could not write ${filename}: ${err.message}`); logger.warn('Could not write ' + filename + ':', err.message);
reject(new errs.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
}); });
}); });
}, }
}; };
export default internalIpRanges; module.exports = internalIpRanges;

View File

@@ -1,15 +1,12 @@
import fs from "node:fs"; const _ = require('lodash');
import { dirname } from "node:path"; const fs = require('fs');
import { fileURLToPath } from "node:url"; const logger = require('../logger').nginx;
import _ from "lodash"; const config = require('../lib/config');
import errs from "../lib/error.js"; const utils = require('../lib/utils');
import utils from "../lib/utils.js"; const error = require('../lib/error');
import { nginx as logger } from "../logger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const internalNginx = { const internalNginx = {
/** /**
* This will: * This will:
* - test the nginx config first to make sure it's OK * - test the nginx config first to make sure it's OK
@@ -27,8 +24,7 @@ const internalNginx = {
configure: (model, host_type, host) => { configure: (model, host_type, host) => {
let combined_meta = {}; let combined_meta = {};
return internalNginx return internalNginx.test()
.test()
.then(() => { .then(() => {
// Nginx is OK // Nginx is OK
// We're deleting this config regardless. // We're deleting this config regardless.
@@ -41,46 +37,49 @@ const internalNginx = {
}) })
.then(() => { .then(() => {
// Test nginx again and update meta with result // Test nginx again and update meta with result
return internalNginx return internalNginx.test()
.test()
.then(() => { .then(() => {
// nginx is ok // nginx is ok
combined_meta = _.assign({}, host.meta, { combined_meta = _.assign({}, host.meta, {
nginx_online: true, nginx_online: true,
nginx_err: null, nginx_err: null
}); });
return model.query().where("id", host.id).patch({ return model
meta: combined_meta, .query()
}); .where('id', host.id)
.patch({
meta: combined_meta
});
}) })
.catch((err) => { .catch((err) => {
// Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported. // Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.
// It will always look like this: // It will always look like this:
// nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address) // nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)
const valid_lines = []; let valid_lines = [];
const err_lines = err.message.split("\n"); let err_lines = err.message.split('\n');
err_lines.map((line) => { err_lines.map(function (line) {
if (line.indexOf("/var/log/nginx/error.log") === -1) { if (line.indexOf('/var/log/nginx/error.log') === -1) {
valid_lines.push(line); valid_lines.push(line);
} }
return true;
}); });
logger.debug("Nginx test failed:", valid_lines.join("\n")); if (config.debug()) {
logger.error('Nginx test failed:', valid_lines.join('\n'));
}
// config is bad, update meta and delete config // config is bad, update meta and delete config
combined_meta = _.assign({}, host.meta, { combined_meta = _.assign({}, host.meta, {
nginx_online: false, nginx_online: false,
nginx_err: valid_lines.join("\n"), nginx_err: valid_lines.join('\n')
}); });
return model return model
.query() .query()
.where("id", host.id) .where('id', host.id)
.patch({ .patch({
meta: combined_meta, meta: combined_meta
}) })
.then(() => { .then(() => {
internalNginx.renameConfigAsError(host_type, host); internalNginx.renameConfigAsError(host_type, host);
@@ -102,18 +101,22 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
test: () => { test: () => {
logger.debug("Testing Nginx configuration"); if (config.debug()) {
return utils.execFile("/usr/sbin/nginx", ["-t", "-g", "error_log off;"]); logger.info('Testing Nginx configuration');
}
return utils.exec('/usr/sbin/nginx -t -g "error_log off;"');
}, },
/** /**
* @returns {Promise} * @returns {Promise}
*/ */
reload: () => { reload: () => {
return internalNginx.test().then(() => { return internalNginx.test()
logger.info("Reloading Nginx"); .then(() => {
return utils.execFile("/usr/sbin/nginx", ["-s", "reload"]); logger.info('Reloading Nginx');
}); return utils.exec('/usr/sbin/nginx -s reload');
});
}, },
/** /**
@@ -122,10 +125,10 @@ const internalNginx = {
* @returns {String} * @returns {String}
*/ */
getConfigName: (host_type, host_id) => { getConfigName: (host_type, host_id) => {
if (host_type === "default") { if (host_type === 'default') {
return "/data/nginx/default_host/site.conf"; return '/data/nginx/default_host/site.conf';
} }
return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`; return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf';
}, },
/** /**
@@ -138,45 +141,38 @@ const internalNginx = {
let template; let template;
try { try {
template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: "utf8" }); template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new errs.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
} }
const renderEngine = utils.getRenderEngine(); const renderEngine = utils.getRenderEngine();
let renderedLocations = ""; let renderedLocations = '';
const locationRendering = async () => { const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) { for (let i = 0; i < host.locations.length; i++) {
const locationCopy = Object.assign( let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id},
{}, {ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits},
{ access_list_id: host.access_list_id }, {allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support},
{ certificate_id: host.certificate_id }, {hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list},
{ ssl_forced: host.ssl_forced }, {certificate: host.certificate}, host.locations[i]);
{ caching_enabled: host.caching_enabled },
{ block_exploits: host.block_exploits },
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
{ http2_support: host.http2_support },
{ hsts_enabled: host.hsts_enabled },
{ hsts_subdomains: host.hsts_subdomains },
{ access_list: host.access_list },
{ certificate: host.certificate },
host.locations[i],
);
if (locationCopy.forward_host.indexOf("/") > -1) { if (locationCopy.forward_host.indexOf('/') > -1) {
const splitted = locationCopy.forward_host.split("/"); const splitted = locationCopy.forward_host.split('/');
locationCopy.forward_host = splitted.shift(); locationCopy.forward_host = splitted.shift();
locationCopy.forward_path = `/${splitted.join("/")}`; locationCopy.forward_path = `/${splitted.join('/')}`;
} }
// eslint-disable-next-line
renderedLocations += await renderEngine.parseAndRender(template, locationCopy); renderedLocations += await renderEngine.parseAndRender(template, locationCopy);
} }
}; };
locationRendering().then(() => resolve(renderedLocations)); locationRendering().then(() => resolve(renderedLocations));
}); });
}, },
@@ -185,23 +181,23 @@ const internalNginx = {
* @param {Object} host * @param {Object} host
* @returns {Promise} * @returns {Promise}
*/ */
generateConfig: (host_type, host_row) => { generateConfig: (host_type, host) => {
// Prevent modifying the original object:
const host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
logger.debug(`Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2)); if (config.debug()) {
logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2));
}
const renderEngine = utils.getRenderEngine(); const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
const filename = internalNginx.getConfigName(nice_host_type, host.id); let filename = internalNginx.getConfigName(nice_host_type, host.id);
try { try {
template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" }); template = fs.readFileSync(__dirname + '/../templates/' + nice_host_type + '.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new errs.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
} }
@@ -209,26 +205,27 @@ const internalNginx = {
let origLocations; let origLocations;
// Manipulate the data a bit before sending it to the template // Manipulate the data a bit before sending it to the template
if (nice_host_type !== "default") { if (nice_host_type !== 'default') {
host.use_default_location = true; host.use_default_location = true;
if (typeof host.advanced_config !== "undefined" && host.advanced_config) { if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
} }
} }
if (host.locations) { if (host.locations) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
origLocations = [].concat(host.locations); origLocations = [].concat(host.locations);
locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
host.locations = renderedLocations; host.locations = renderedLocations;
}); });
// Allow someone who is using / custom location path to use it, and skip the default / location // Allow someone who is using / custom location path to use it, and skip the default / location
_.map(host.locations, (location) => { _.map(host.locations, (location) => {
if (location.path === "/") { if (location.path === '/') {
host.use_default_location = false; host.use_default_location = false;
} }
}); });
} else { } else {
locationsPromise = Promise.resolve(); locationsPromise = Promise.resolve();
} }
@@ -240,8 +237,11 @@ const internalNginx = {
renderEngine renderEngine
.parseAndRender(template, host) .parseAndRender(template, host)
.then((config_text) => { .then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" }); fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
logger.debug("Wrote config:", filename, config_text);
if (config.debug()) {
logger.success('Wrote config:', filename, config_text);
}
// Restore locations array // Restore locations array
host.locations = origLocations; host.locations = origLocations;
@@ -249,8 +249,11 @@ const internalNginx = {
resolve(true); resolve(true);
}) })
.catch((err) => { .catch((err) => {
logger.debug(`Could not write ${filename}:`, err.message); if (config.debug()) {
reject(new errs.ConfigurationError(err.message)); logger.warn('Could not write ' + filename + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
}); });
}); });
}); });
@@ -265,17 +268,20 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
generateLetsEncryptRequestConfig: (certificate) => { generateLetsEncryptRequestConfig: (certificate) => {
logger.debug("Generating LetsEncrypt Request Config:", certificate); if (config.debug()) {
logger.info('Generating LetsEncrypt Request Config:', certificate);
}
const renderEngine = utils.getRenderEngine(); const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
try { try {
template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: "utf8" }); template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
reject(new errs.ConfigurationError(err.message)); reject(new error.ConfigurationError(err.message));
return; return;
} }
@@ -284,13 +290,20 @@ const internalNginx = {
renderEngine renderEngine
.parseAndRender(template, certificate) .parseAndRender(template, certificate)
.then((config_text) => { .then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" }); fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
logger.debug("Wrote config:", filename, config_text);
if (config.debug()) {
logger.success('Wrote config:', filename, config_text);
}
resolve(true); resolve(true);
}) })
.catch((err) => { .catch((err) => {
logger.debug(`Could not write ${filename}:`, err.message); if (config.debug()) {
reject(new errs.ConfigurationError(err.message)); logger.warn('Could not write ' + filename + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
}); });
}); });
}, },
@@ -301,11 +314,11 @@ const internalNginx = {
* @param {String} filename * @param {String} filename
*/ */
deleteFile: (filename) => { deleteFile: (filename) => {
logger.debug(`Deleting file: ${filename}`); logger.debug('Deleting file: ' + filename);
try { try {
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));
} }
}, },
@@ -315,7 +328,7 @@ const internalNginx = {
* @returns String * @returns String
*/ */
getFileFriendlyHostType: (host_type) => { getFileFriendlyHostType: (host_type) => {
return host_type.replace(/-/g, "_"); return host_type.replace(new RegExp('-', 'g'), '_');
}, },
/** /**
@@ -325,8 +338,8 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
deleteLetsEncryptRequestConfig: (certificate) => { deleteLetsEncryptRequestConfig: (certificate) => {
const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; const config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
return new Promise((resolve /*, reject*/) => { return new Promise((resolve/*, reject*/) => {
internalNginx.deleteFile(config_file); internalNginx.deleteFile(config_file);
resolve(); resolve();
}); });
@@ -339,13 +352,10 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
deleteConfig: (host_type, host, delete_err_file) => { deleteConfig: (host_type, host, delete_err_file) => {
const config_file = internalNginx.getConfigName( const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);
internalNginx.getFileFriendlyHostType(host_type), const config_file_err = config_file + '.err';
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
return new Promise((resolve /*, reject*/) => { return new Promise((resolve/*, reject*/) => {
internalNginx.deleteFile(config_file); internalNginx.deleteFile(config_file);
if (delete_err_file) { if (delete_err_file) {
internalNginx.deleteFile(config_file_err); internalNginx.deleteFile(config_file_err);
@@ -360,13 +370,10 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
renameConfigAsError: (host_type, host) => { renameConfigAsError: (host_type, host) => {
const config_file = internalNginx.getConfigName( const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);
internalNginx.getFileFriendlyHostType(host_type), const config_file_err = config_file + '.err';
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
return new Promise((resolve /*, reject*/) => { return new Promise((resolve/*, reject*/) => {
fs.unlink(config_file, () => { fs.unlink(config_file, () => {
// ignore result, continue // ignore result, continue
fs.rename(config_file, config_file_err, () => { fs.rename(config_file, config_file_err, () => {
@@ -383,10 +390,9 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
bulkGenerateConfigs: (host_type, hosts) => { bulkGenerateConfigs: (host_type, hosts) => {
const promises = []; let promises = [];
hosts.map((host) => { hosts.map(function (host) {
promises.push(internalNginx.generateConfig(host_type, host)); promises.push(internalNginx.generateConfig(host_type, host));
return true;
}); });
return Promise.all(promises); return Promise.all(promises);
@@ -398,10 +404,9 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
bulkDeleteConfigs: (host_type, hosts) => { bulkDeleteConfigs: (host_type, hosts) => {
const promises = []; let promises = [];
hosts.map((host) => { hosts.map(function (host) {
promises.push(internalNginx.deleteConfig(host_type, host, true)); promises.push(internalNginx.deleteConfig(host_type, host, true));
return true;
}); });
return Promise.all(promises); return Promise.all(promises);
@@ -411,19 +416,21 @@ const internalNginx = {
* @param {string} config * @param {string} config
* @returns {boolean} * @returns {boolean}
*/ */
advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im), advancedConfigHasDefaultLocation: function (cfg) {
return !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
},
/** /**
* @returns {boolean} * @returns {boolean}
*/ */
ipv6Enabled: () => { ipv6Enabled: function () {
if (typeof process.env.DISABLE_IPV6 !== "undefined") { if (typeof process.env.DISABLE_IPV6 !== 'undefined') {
const disabled = process.env.DISABLE_IPV6.toLowerCase(); const disabled = process.env.DISABLE_IPV6.toLowerCase();
return !(disabled === "on" || disabled === "true" || disabled === "1" || disabled === "yes"); return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');
} }
return true; return true;
}, }
}; };
export default internalNginx; module.exports = internalNginx;

View File

@@ -1,106 +1,100 @@
import _ from "lodash"; const _ = require('lodash');
import errs from "../lib/error.js"; const error = require('../lib/error');
import { castJsonIfNeed } from "../lib/helpers.js"; const utils = require('../lib/utils');
import utils from "../lib/utils.js"; const proxyHostModel = require('../models/proxy_host');
import proxyHostModel from "../models/proxy_host.js"; const internalHost = require('./host');
import internalAuditLog from "./audit-log.js"; const internalNginx = require('./nginx');
import internalCertificate from "./certificate.js"; const internalAuditLog = require('./audit-log');
import internalHost from "./host.js"; const internalCertificate = require('./certificate');
import internalNginx from "./nginx.js";
const omissions = () => { function omissions () {
return ["is_deleted", "owner.is_deleted"]; return ['is_deleted'];
}; }
const internalProxyHost = { const internalProxyHost = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @returns {Promise} * @returns {Promise}
*/ */
create: (access, data) => { create: (access, data) => {
let thisData = data; let create_certificate = data.certificate_id === 'new';
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) { if (create_certificate) {
delete thisData.certificate_id; delete data.certificate_id;
} }
return access return access.can('proxy_hosts:create', data)
.can("proxy_hosts:create", thisData)
.then(() => { .then(() => {
// Get a list of the domain names and check each of them against existing records // Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = []; let domain_name_check_promises = [];
thisData.domain_names.map((domain_name) => { data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
return true;
}); });
return Promise.all(domain_name_check_promises).then((check_results) => { return Promise.all(domain_name_check_promises)
check_results.map((result) => { .then((check_results) => {
if (result.is_taken) { check_results.map(function (result) {
throw new errs.ValidationError(`${result.hostname} is already in use`); if (result.is_taken) {
} throw new error.ValidationError(result.hostname + ' is already in use');
return true; }
});
}); });
});
}) })
.then(() => { .then(() => {
// At this point the domains should have been checked // At this point the domains should have been checked
thisData.owner_user_id = access.token.getUserId(1); data.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData); data = internalHost.cleanSslHstsData(data);
// Fix for db field not having a default value return proxyHostModel
// for this optional field. .query()
if (typeof thisData.advanced_config === "undefined") { .insertAndFetch(data)
thisData.advanced_config = ""; .then(utils.omitRow(omissions()));
}
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
}) })
.then((row) => { .then((row) => {
if (createCertificate) { if (create_certificate) {
return internalCertificate return internalCertificate.createQuickCertificate(access, data)
.createQuickCertificate(access, thisData)
.then((cert) => { .then((cert) => {
// update host with cert id // update host with cert id
return internalProxyHost.update(access, { return internalProxyHost.update(access, {
id: row.id, id: row.id,
certificate_id: cert.id, certificate_id: cert.id
}); });
}) })
.then(() => { .then(() => {
return row; return row;
}); });
} else {
return row;
} }
return row;
}) })
.then((row) => { .then((row) => {
// re-fetch with cert // re-fetch with cert
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: row.id, id: row.id,
expand: ["certificate", "owner", "access_list.[clients,items]"], expand: ['certificate', 'owner', 'access_list.[clients,items]']
}); });
}) })
.then((row) => { .then((row) => {
// Configure nginx // Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => { return internalNginx.configure(proxyHostModel, 'proxy_host', row)
return row; .then(() => {
}); return row;
});
}) })
.then((row) => { .then((row) => {
// Audit log // Audit log
thisData.meta = _.assign({}, thisData.meta || {}, row.meta); data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log // Add to audit log
return internalAuditLog return internalAuditLog.add(access, {
.add(access, { action: 'created',
action: "created", object_type: 'proxy-host',
object_type: "proxy-host", object_id: row.id,
object_id: row.id, meta: data
meta: thisData, })
})
.then(() => { .then(() => {
return row; return row;
}); });
@@ -114,110 +108,100 @@ const internalProxyHost = {
* @return {Promise} * @return {Promise}
*/ */
update: (access, data) => { update: (access, data) => {
let thisData = data; let create_certificate = data.certificate_id === 'new';
const create_certificate = thisData.certificate_id === "new";
if (create_certificate) { if (create_certificate) {
delete thisData.certificate_id; delete data.certificate_id;
} }
return access return access.can('proxy_hosts:update', data.id)
.can("proxy_hosts:update", thisData.id)
.then((/*access_data*/) => { .then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records // Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = []; let domain_name_check_promises = [];
if (typeof thisData.domain_names !== "undefined") { if (typeof data.domain_names !== 'undefined') {
thisData.domain_names.map((domain_name) => { data.domain_names.map(function (domain_name) {
return domain_name_check_promises.push( domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'proxy', data.id));
internalHost.isHostnameTaken(domain_name, "proxy", thisData.id),
);
}); });
return Promise.all(domain_name_check_promises).then((check_results) => { return Promise.all(domain_name_check_promises)
check_results.map((result) => { .then((check_results) => {
if (result.is_taken) { check_results.map(function (result) {
throw new errs.ValidationError(`${result.hostname} is already in use`); if (result.is_taken) {
} throw new error.ValidationError(result.hostname + ' is already in use');
return true; }
});
}); });
});
} }
}) })
.then(() => { .then(() => {
return internalProxyHost.get(access, { id: thisData.id }); return internalProxyHost.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (row.id !== thisData.id) { if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError( throw new error.InternalValidationError('Proxy Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
`Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
} }
if (create_certificate) { if (create_certificate) {
return internalCertificate return internalCertificate.createQuickCertificate(access, {
.createQuickCertificate(access, { domain_names: data.domain_names || row.domain_names,
domain_names: thisData.domain_names || row.domain_names, meta: _.assign({}, row.meta, data.meta)
meta: _.assign({}, row.meta, thisData.meta), })
})
.then((cert) => { .then((cert) => {
// update host with cert id // update host with cert id
thisData.certificate_id = cert.id; data.certificate_id = cert.id;
}) })
.then(() => { .then(() => {
return row; return row;
}); });
} else {
return row;
} }
return row;
}) })
.then((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. // 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( data = _.assign({}, {
{}, domain_names: row.domain_names
{ }, data);
domain_names: row.domain_names,
},
data,
);
thisData = internalHost.cleanSslHstsData(thisData, row); data = internalHost.cleanSslHstsData(data, row);
return proxyHostModel return proxyHostModel
.query() .query()
.where({ id: thisData.id }) .where({id: data.id})
.patch(thisData) .patch(data)
.then(utils.omitRow(omissions())) .then(utils.omitRow(omissions()))
.then((saved_row) => { .then((saved_row) => {
// Add to audit log // Add to audit log
return internalAuditLog return internalAuditLog.add(access, {
.add(access, { action: 'updated',
action: "updated", object_type: 'proxy-host',
object_type: "proxy-host", object_id: row.id,
object_id: row.id, meta: data
meta: thisData, })
})
.then(() => { .then(() => {
return saved_row; return saved_row;
}); });
}); });
}) })
.then(() => { .then(() => {
return internalProxyHost return internalProxyHost.get(access, {
.get(access, { id: data.id,
id: thisData.id, expand: ['owner', 'certificate', 'access_list.[clients,items]']
expand: ["owner", "certificate", "access_list.[clients,items]"], })
})
.then((row) => { .then((row) => {
if (!row.enabled) { if (!row.enabled) {
// No need to add nginx config if host is disabled // No need to add nginx config if host is disabled
return row; return row;
} }
// Configure nginx // Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => { return internalNginx.configure(proxyHostModel, 'proxy_host', row)
row.meta = new_meta; .then((new_meta) => {
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); row.meta = new_meta;
}); row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
}); });
}); });
}, },
@@ -231,38 +215,39 @@ const internalProxyHost = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
const thisData = data || {}; if (typeof data === 'undefined') {
data = {};
}
return access return access.can('proxy_hosts:get', data.id)
.can("proxy_hosts:get", thisData.id)
.then((access_data) => { .then((access_data) => {
const query = proxyHostModel let query = proxyHostModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.andWhere("id", thisData.id) .andWhere('id', data.id)
.allowGraph("[owner,access_list.[clients,items],certificate]") .allowGraph('[owner,access_list.[clients,items],certificate]')
.first(); .first();
if (access_data.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));
} }
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`); query.withGraphFetched('[' + data.expand.join(', ') + ']');
} }
return query.then(utils.omitRow(omissions())); return query.then(utils.omitRow(omissions()));
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(thisData.id); throw new error.ItemNotFoundError(data.id);
} }
const thisRow = internalHost.cleanRowCertificateMeta(row); row = internalHost.cleanRowCertificateMeta(row);
// Custom omissions // Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
return _.omit(row, thisData.omit); row = _.omit(row, data.omit);
} }
return thisRow; return row;
}); });
}, },
@@ -274,35 +259,35 @@ const internalProxyHost = {
* @returns {Promise} * @returns {Promise}
*/ */
delete: (access, data) => { delete: (access, data) => {
return access return access.can('proxy_hosts:delete', data.id)
.can("proxy_hosts:delete", data.id)
.then(() => { .then(() => {
return internalProxyHost.get(access, { id: data.id }); return internalProxyHost.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} }
return proxyHostModel return proxyHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
is_deleted: 1, is_deleted: 1
}) })
.then(() => { .then(() => {
// Delete Nginx Config // Delete Nginx Config
return internalNginx.deleteConfig("proxy_host", row).then(() => { return internalNginx.deleteConfig('proxy_host', row)
return internalNginx.reload(); .then(() => {
}); return internalNginx.reload();
});
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "deleted", action: 'deleted',
object_type: "proxy-host", object_type: 'proxy-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -319,41 +304,39 @@ const internalProxyHost = {
* @returns {Promise} * @returns {Promise}
*/ */
enable: (access, data) => { enable: (access, data) => {
return access return access.can('proxy_hosts:update', data.id)
.can("proxy_hosts:update", data.id)
.then(() => { .then(() => {
return internalProxyHost.get(access, { return internalProxyHost.get(access, {
id: data.id, id: data.id,
expand: ["certificate", "owner", "access_list"], expand: ['certificate', 'owner', 'access_list']
}); });
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} } else if (row.enabled) {
if (row.enabled) { throw new error.ValidationError('Host is already enabled');
throw new errs.ValidationError("Host is already enabled");
} }
row.enabled = 1; row.enabled = 1;
return proxyHostModel return proxyHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 1, enabled: 1
}) })
.then(() => { .then(() => {
// Configure nginx // Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row); return internalNginx.configure(proxyHostModel, 'proxy_host', row);
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "enabled", action: 'enabled',
object_type: "proxy-host", object_type: 'proxy-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -370,40 +353,39 @@ const internalProxyHost = {
* @returns {Promise} * @returns {Promise}
*/ */
disable: (access, data) => { disable: (access, data) => {
return access return access.can('proxy_hosts:update', data.id)
.can("proxy_hosts:update", data.id)
.then(() => { .then(() => {
return internalProxyHost.get(access, { id: data.id }); return internalProxyHost.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} } else if (!row.enabled) {
if (!row.enabled) { throw new error.ValidationError('Host is already disabled');
throw new errs.ValidationError("Host is already disabled");
} }
row.enabled = 0; row.enabled = 0;
return proxyHostModel return proxyHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 0, enabled: 0
}) })
.then(() => { .then(() => {
// Delete Nginx Config // Delete Nginx Config
return internalNginx.deleteConfig("proxy_host", row).then(() => { return internalNginx.deleteConfig('proxy_host', row)
return internalNginx.reload(); .then(() => {
}); return internalNginx.reload();
});
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "disabled", action: 'disabled',
object_type: "proxy-host", object_type: 'proxy-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -420,38 +402,40 @@ 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.can('proxy_hosts:list')
.then((access_data) => {
let query = proxyHostModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,access_list,certificate]')
.orderBy('domain_names', 'ASC');
const query = proxyHostModel if (access_data.permission_visibility !== 'all') {
.query() query.andWhere('owner_user_id', access.token.getUserId(1));
.where("is_deleted", 0) }
.groupBy("id")
.allowGraph("[owner,access_list,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") { // Query is used for searching
query.andWhere("owner_user_id", access.token.getUserId(1)); if (typeof search_query === 'string') {
} query.where(function () {
this.where('domain_names', 'like', '%' + search_query + '%');
});
}
// 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("domain_names"), "like", `%${searchQuery}%`);
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;
}, },
/** /**
@@ -462,16 +446,20 @@ const internalProxyHost = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: (user_id, visibility) => { getCount: (user_id, visibility) => {
const query = proxyHostModel.query().count("id as count").where("is_deleted", 0); let query = proxyHostModel
.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);
} }
return query.first().then((row) => { return query.first()
return Number.parseInt(row.count, 10); .then((row) => {
}); return parseInt(row.count, 10);
}, });
}
}; };
export default internalProxyHost; module.exports = internalProxyHost;

View File

@@ -1,73 +1,66 @@
import _ from "lodash"; const _ = require('lodash');
import errs from "../lib/error.js"; const error = require('../lib/error');
import { castJsonIfNeed } from "../lib/helpers.js"; const utils = require('../lib/utils');
import utils from "../lib/utils.js"; const redirectionHostModel = require('../models/redirection_host');
import redirectionHostModel from "../models/redirection_host.js"; const internalHost = require('./host');
import internalAuditLog from "./audit-log.js"; const internalNginx = require('./nginx');
import internalCertificate from "./certificate.js"; const internalAuditLog = require('./audit-log');
import internalHost from "./host.js"; const internalCertificate = require('./certificate');
import internalNginx from "./nginx.js";
const omissions = () => { function omissions () {
return ["is_deleted"]; return ['is_deleted'];
}; }
const internalRedirectionHost = { const internalRedirectionHost = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @returns {Promise} * @returns {Promise}
*/ */
create: (access, data) => { create: (access, data) => {
let thisData = data || {}; let create_certificate = data.certificate_id === 'new';
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) { if (create_certificate) {
delete thisData.certificate_id; delete data.certificate_id;
} }
return access return access.can('redirection_hosts:create', data)
.can("redirection_hosts:create", thisData)
.then((/*access_data*/) => { .then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records // Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = []; let domain_name_check_promises = [];
thisData.domain_names.map((domain_name) => { data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
return true;
}); });
return Promise.all(domain_name_check_promises).then((check_results) => { return Promise.all(domain_name_check_promises)
check_results.map((result) => { .then((check_results) => {
if (result.is_taken) { check_results.map(function (result) {
throw new errs.ValidationError(`${result.hostname} is already in use`); if (result.is_taken) {
} throw new error.ValidationError(result.hostname + ' is already in use');
return true; }
});
}); });
});
}) })
.then(() => { .then(() => {
// At this point the domains should have been checked // At this point the domains should have been checked
thisData.owner_user_id = access.token.getUserId(1); data.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData); data = internalHost.cleanSslHstsData(data);
// Fix for db field not having a default value return redirectionHostModel
// for this optional field. .query()
if (typeof data.advanced_config === "undefined") { .insertAndFetch(data)
data.advanced_config = ""; .then(utils.omitRow(omissions()));
}
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
}) })
.then((row) => { .then((row) => {
if (createCertificate) { if (create_certificate) {
return internalCertificate return internalCertificate.createQuickCertificate(access, data)
.createQuickCertificate(access, thisData)
.then((cert) => { .then((cert) => {
// update host with cert id // update host with cert id
return internalRedirectionHost.update(access, { return internalRedirectionHost.update(access, {
id: row.id, id: row.id,
certificate_id: cert.id, certificate_id: cert.id
}); });
}) })
.then(() => { .then(() => {
@@ -79,27 +72,27 @@ const internalRedirectionHost = {
.then((row) => { .then((row) => {
// re-fetch with cert // re-fetch with cert
return internalRedirectionHost.get(access, { return internalRedirectionHost.get(access, {
id: row.id, id: row.id,
expand: ["certificate", "owner"], expand: ['certificate', 'owner']
}); });
}) })
.then((row) => { .then((row) => {
// Configure nginx // Configure nginx
return internalNginx.configure(redirectionHostModel, "redirection_host", row).then(() => { return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
return row; .then(() => {
}); return row;
});
}) })
.then((row) => { .then((row) => {
thisData.meta = _.assign({}, thisData.meta || {}, row.meta); data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log // Add to audit log
return internalAuditLog return internalAuditLog.add(access, {
.add(access, { action: 'created',
action: "created", object_type: 'redirection-host',
object_type: "redirection-host", object_id: row.id,
object_id: row.id, meta: data
meta: thisData, })
})
.then(() => { .then(() => {
return row; return row;
}); });
@@ -113,107 +106,94 @@ const internalRedirectionHost = {
* @return {Promise} * @return {Promise}
*/ */
update: (access, data) => { update: (access, data) => {
let thisData = data || {}; let create_certificate = data.certificate_id === 'new';
const createCertificate = thisData.certificate_id === "new";
if (createCertificate) { if (create_certificate) {
delete thisData.certificate_id; delete data.certificate_id;
} }
return access return access.can('redirection_hosts:update', data.id)
.can("redirection_hosts:update", thisData.id)
.then((/*access_data*/) => { .then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records // Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = []; let domain_name_check_promises = [];
if (typeof thisData.domain_names !== "undefined") { if (typeof data.domain_names !== 'undefined') {
thisData.domain_names.map((domain_name) => { data.domain_names.map(function (domain_name) {
domain_name_check_promises.push( domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'redirection', data.id));
internalHost.isHostnameTaken(domain_name, "redirection", thisData.id),
);
return true;
}); });
return Promise.all(domain_name_check_promises).then((check_results) => { return Promise.all(domain_name_check_promises)
check_results.map((result) => { .then((check_results) => {
if (result.is_taken) { check_results.map(function (result) {
throw new errs.ValidationError(`${result.hostname} is already in use`); if (result.is_taken) {
} throw new error.ValidationError(result.hostname + ' is already in use');
return true; }
});
}); });
});
} }
}) })
.then(() => { .then(() => {
return internalRedirectionHost.get(access, { id: thisData.id }); return internalRedirectionHost.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (row.id !== thisData.id) { if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError( throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
`Redirection Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
} }
if (createCertificate) { if (create_certificate) {
return internalCertificate return internalCertificate.createQuickCertificate(access, {
.createQuickCertificate(access, { domain_names: data.domain_names || row.domain_names,
domain_names: thisData.domain_names || row.domain_names, meta: _.assign({}, row.meta, data.meta)
meta: _.assign({}, row.meta, thisData.meta), })
})
.then((cert) => { .then((cert) => {
// update host with cert id // update host with cert id
thisData.certificate_id = cert.id; data.certificate_id = cert.id;
}) })
.then(() => { .then(() => {
return row; return row;
}); });
} else {
return row;
} }
return row;
}) })
.then((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. // 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( data = _.assign({}, {
{}, domain_names: row.domain_names
{ }, data);
domain_names: row.domain_names,
},
thisData,
);
thisData = internalHost.cleanSslHstsData(thisData, row); data = internalHost.cleanSslHstsData(data, row);
return redirectionHostModel return redirectionHostModel
.query() .query()
.where({ id: thisData.id }) .where({id: data.id})
.patch(thisData) .patch(data)
.then((saved_row) => { .then((saved_row) => {
// Add to audit log // Add to audit log
return internalAuditLog return internalAuditLog.add(access, {
.add(access, { action: 'updated',
action: "updated", object_type: 'redirection-host',
object_type: "redirection-host", object_id: row.id,
object_id: row.id, meta: data
meta: thisData, })
})
.then(() => { .then(() => {
return _.omit(saved_row, omissions()); return _.omit(saved_row, omissions());
}); });
}); });
}) })
.then(() => { .then(() => {
return internalRedirectionHost return internalRedirectionHost.get(access, {
.get(access, { id: data.id,
id: thisData.id, expand: ['owner', 'certificate']
expand: ["owner", "certificate"], })
})
.then((row) => { .then((row) => {
// Configure nginx // Configure nginx
return internalNginx return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
.configure(redirectionHostModel, "redirection_host", row)
.then((new_meta) => { .then((new_meta) => {
row.meta = new_meta; row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
}); });
}); });
}); });
@@ -228,39 +208,39 @@ const internalRedirectionHost = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
const thisData = data || {}; if (typeof data === 'undefined') {
data = {};
}
return access return access.can('redirection_hosts:get', data.id)
.can("redirection_hosts:get", thisData.id)
.then((access_data) => { .then((access_data) => {
const query = redirectionHostModel let query = redirectionHostModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.andWhere("id", thisData.id) .andWhere('id', data.id)
.allowGraph("[owner,certificate]") .allowGraph('[owner,certificate]')
.first(); .first();
if (access_data.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));
} }
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`); query.withGraphFetched('[' + data.expand.join(', ') + ']');
} }
return query.then(utils.omitRow(omissions())); return query.then(utils.omitRow(omissions()));
}) })
.then((row) => { .then((row) => {
let thisRow = row; if (!row) {
if (!thisRow || !thisRow.id) { throw new error.ItemNotFoundError(data.id);
throw new errs.ItemNotFoundError(thisData.id);
} }
thisRow = internalHost.cleanRowCertificateMeta(thisRow); row = internalHost.cleanRowCertificateMeta(row);
// Custom omissions // Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
return _.omit(thisRow, thisData.omit); row = _.omit(row, data.omit);
} }
return thisRow; return row;
}); });
}, },
@@ -272,35 +252,35 @@ const internalRedirectionHost = {
* @returns {Promise} * @returns {Promise}
*/ */
delete: (access, data) => { delete: (access, data) => {
return access return access.can('redirection_hosts:delete', data.id)
.can("redirection_hosts:delete", data.id)
.then(() => { .then(() => {
return internalRedirectionHost.get(access, { id: data.id }); return internalRedirectionHost.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} }
return redirectionHostModel return redirectionHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
is_deleted: 1, is_deleted: 1
}) })
.then(() => { .then(() => {
// Delete Nginx Config // Delete Nginx Config
return internalNginx.deleteConfig("redirection_host", row).then(() => { return internalNginx.deleteConfig('redirection_host', row)
return internalNginx.reload(); .then(() => {
}); return internalNginx.reload();
});
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "deleted", action: 'deleted',
object_type: "redirection-host", object_type: 'redirection-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -317,41 +297,39 @@ const internalRedirectionHost = {
* @returns {Promise} * @returns {Promise}
*/ */
enable: (access, data) => { enable: (access, data) => {
return access return access.can('redirection_hosts:update', data.id)
.can("redirection_hosts:update", data.id)
.then(() => { .then(() => {
return internalRedirectionHost.get(access, { return internalRedirectionHost.get(access, {
id: data.id, id: data.id,
expand: ["certificate", "owner"], expand: ['certificate', 'owner']
}); });
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} } else if (row.enabled) {
if (row.enabled) { throw new error.ValidationError('Host is already enabled');
throw new errs.ValidationError("Host is already enabled");
} }
row.enabled = 1; row.enabled = 1;
return redirectionHostModel return redirectionHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 1, enabled: 1
}) })
.then(() => { .then(() => {
// Configure nginx // Configure nginx
return internalNginx.configure(redirectionHostModel, "redirection_host", row); return internalNginx.configure(redirectionHostModel, 'redirection_host', row);
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "enabled", action: 'enabled',
object_type: "redirection-host", object_type: 'redirection-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -368,40 +346,39 @@ const internalRedirectionHost = {
* @returns {Promise} * @returns {Promise}
*/ */
disable: (access, data) => { disable: (access, data) => {
return access return access.can('redirection_hosts:update', data.id)
.can("redirection_hosts:update", data.id)
.then(() => { .then(() => {
return internalRedirectionHost.get(access, { id: data.id }); return internalRedirectionHost.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} } else if (!row.enabled) {
if (!row.enabled) { throw new error.ValidationError('Host is already disabled');
throw new errs.ValidationError("Host is already disabled");
} }
row.enabled = 0; row.enabled = 0;
return redirectionHostModel return redirectionHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 0, enabled: 0
}) })
.then(() => { .then(() => {
// Delete Nginx Config // Delete Nginx Config
return internalNginx.deleteConfig("redirection_host", row).then(() => { return internalNginx.deleteConfig('redirection_host', row)
return internalNginx.reload(); .then(() => {
}); return internalNginx.reload();
});
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "disabled", action: 'disabled',
object_type: "redirection-host", object_type: 'redirection-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -419,35 +396,34 @@ const internalRedirectionHost = {
* @returns {Promise} * @returns {Promise}
*/ */
getAll: (access, expand, search_query) => { getAll: (access, expand, search_query) => {
return access return access.can('redirection_hosts:list')
.can("redirection_hosts:list")
.then((access_data) => { .then((access_data) => {
const query = redirectionHostModel let query = redirectionHostModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.groupBy("id") .groupBy('id')
.allowGraph("[owner,certificate]") .allowGraph('[owner,certificate]')
.orderBy(castJsonIfNeed("domain_names"), "ASC"); .orderBy('domain_names', 'ASC');
if (access_data.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 search_query === "string" && search_query.length > 0) { if (typeof search_query === 'string') {
query.where(function () { query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`); this.where('domain_names', 'like', '%' + search_query + '%');
}); });
} }
if (typeof expand !== "undefined" && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`); query.withGraphFetched('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query.then(utils.omitRows(omissions()));
}) })
.then((rows) => { .then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows); return internalHost.cleanAllRowsCertificateMeta(rows);
} }
@@ -463,16 +439,20 @@ const internalRedirectionHost = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: (user_id, visibility) => { getCount: (user_id, visibility) => {
const query = redirectionHostModel.query().count("id as count").where("is_deleted", 0); let query = redirectionHostModel
.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);
} }
return query.first().then((row) => { return query.first()
return Number.parseInt(row.count, 10); .then((row) => {
}); return parseInt(row.count, 10);
}, });
}
}; };
export default internalRedirectionHost; module.exports = internalRedirectionHost;

View File

@@ -1,37 +1,38 @@
import internalDeadHost from "./dead-host.js"; const internalProxyHost = require('./proxy-host');
import internalProxyHost from "./proxy-host.js"; const internalRedirectionHost = require('./redirection-host');
import internalRedirectionHost from "./redirection-host.js"; const internalDeadHost = require('./dead-host');
import internalStream from "./stream.js"; const internalStream = require('./stream');
const internalReport = { const internalReport = {
/** /**
* @param {Access} access * @param {Access} access
* @return {Promise} * @return {Promise}
*/ */
getHostsReport: (access) => { getHostsReport: (access) => {
return access return access.can('reports:hosts', 1)
.can("reports:hosts", 1)
.then((access_data) => { .then((access_data) => {
const userId = access.token.getUserId(1); let user_id = access.token.getUserId(1);
const promises = [ let promises = [
internalProxyHost.getCount(userId, access_data.visibility), internalProxyHost.getCount(user_id, access_data.visibility),
internalRedirectionHost.getCount(userId, access_data.visibility), internalRedirectionHost.getCount(user_id, access_data.visibility),
internalStream.getCount(userId, access_data.visibility), internalStream.getCount(user_id, access_data.visibility),
internalDeadHost.getCount(userId, access_data.visibility), internalDeadHost.getCount(user_id, access_data.visibility)
]; ];
return Promise.all(promises); return Promise.all(promises);
}) })
.then((counts) => { .then((counts) => {
return { return {
proxy: counts.shift(), proxy: counts.shift(),
redirection: counts.shift(), redirection: counts.shift(),
stream: counts.shift(), stream: counts.shift(),
dead: counts.shift(), dead: counts.shift()
}; };
}); });
},
}
}; };
export default internalReport; module.exports = internalReport;

View File

@@ -1,9 +1,10 @@
import fs from "node:fs"; const fs = require('fs');
import errs from "../lib/error.js"; const error = require('../lib/error');
import settingModel from "../models/setting.js"; const settingModel = require('../models/setting');
import internalNginx from "./nginx.js"; const internalNginx = require('./nginx');
const internalSetting = { const internalSetting = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
@@ -11,38 +12,37 @@ const internalSetting = {
* @return {Promise} * @return {Promise}
*/ */
update: (access, data) => { update: (access, data) => {
return access return access.can('settings:update', data.id)
.can("settings:update", data.id)
.then((/*access_data*/) => { .then((/*access_data*/) => {
return internalSetting.get(access, { id: data.id }); return internalSetting.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (row.id !== data.id) { if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError( throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
`Setting could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
} }
return settingModel.query().where({ id: data.id }).patch(data); return settingModel
.query()
.where({id: data.id})
.patch(data);
}) })
.then(() => { .then(() => {
return internalSetting.get(access, { return internalSetting.get(access, {
id: data.id, id: data.id
}); });
}) })
.then((row) => { .then((row) => {
if (row.id === "default-site") { if (row.id === 'default-site') {
// write the html if we need to // write the html if we need to
if (row.value === "html") { if (row.value === 'html') {
fs.writeFileSync("/data/nginx/default_www/index.html", row.meta.html, { encoding: "utf8" }); fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'});
} }
// Configure nginx // Configure nginx
return internalNginx return internalNginx.deleteConfig('default')
.deleteConfig("default")
.then(() => { .then(() => {
return internalNginx.generateConfig("default", row); return internalNginx.generateConfig('default', row);
}) })
.then(() => { .then(() => {
return internalNginx.test(); return internalNginx.test();
@@ -54,8 +54,7 @@ const internalSetting = {
return row; return row;
}) })
.catch((/*err*/) => { .catch((/*err*/) => {
internalNginx internalNginx.deleteConfig('default')
.deleteConfig("default")
.then(() => { .then(() => {
return internalNginx.test(); return internalNginx.test();
}) })
@@ -64,11 +63,12 @@ const internalSetting = {
}) })
.then(() => { .then(() => {
// I'm being slack here I know.. // I'm being slack here I know..
throw new errs.ValidationError("Could not reconfigure Nginx. Please check logs."); throw new error.ValidationError('Could not reconfigure Nginx. Please check logs.');
}); });
}); });
} else {
return row;
} }
return row;
}); });
}, },
@@ -79,16 +79,19 @@ const internalSetting = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
return access return access.can('settings:get', data.id)
.can("settings:get", data.id)
.then(() => { .then(() => {
return settingModel.query().where("id", data.id).first(); return settingModel
.query()
.where('id', data.id)
.first();
}) })
.then((row) => { .then((row) => {
if (row) { if (row) {
return row; return row;
} else {
throw new error.ItemNotFoundError(data.id);
} }
throw new errs.ItemNotFoundError(data.id);
}); });
}, },
@@ -99,13 +102,15 @@ const internalSetting = {
* @returns {*} * @returns {*}
*/ */
getCount: (access) => { getCount: (access) => {
return access return access.can('settings:list')
.can("settings:list")
.then(() => { .then(() => {
return settingModel.query().count("id as count").first(); return settingModel
.query()
.count('id as count')
.first();
}) })
.then((row) => { .then((row) => {
return Number.parseInt(row.count, 10); return parseInt(row.count, 10);
}); });
}, },
@@ -116,10 +121,13 @@ const internalSetting = {
* @returns {Promise} * @returns {Promise}
*/ */
getAll: (access) => { getAll: (access) => {
return access.can("settings:list").then(() => { return access.can('settings:list')
return settingModel.query().orderBy("description", "ASC"); .then(() => {
}); return settingModel
}, .query()
.orderBy('description', 'ASC');
});
}
}; };
export default internalSetting; module.exports = internalSetting;

View File

@@ -1,85 +1,51 @@
import _ from "lodash"; const _ = require('lodash');
import errs from "../lib/error.js"; const error = require('../lib/error');
import { castJsonIfNeed } from "../lib/helpers.js"; const utils = require('../lib/utils');
import utils from "../lib/utils.js"; const streamModel = require('../models/stream');
import streamModel from "../models/stream.js"; const internalNginx = require('./nginx');
import internalAuditLog from "./audit-log.js"; const internalAuditLog = require('./audit-log');
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const omissions = () => { function omissions () {
return ["is_deleted", "owner.is_deleted", "certificate.is_deleted"]; return ['is_deleted'];
}; }
const internalStream = { const internalStream = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @returns {Promise} * @returns {Promise}
*/ */
create: (access, data) => { create: (access, data) => {
const create_certificate = data.certificate_id === "new"; return access.can('streams:create', data)
if (create_certificate) {
delete data.certificate_id;
}
return access
.can("streams:create", data)
.then((/*access_data*/) => { .then((/*access_data*/) => {
// TODO: At this point the existing ports should have been checked // TODO: At this point the existing ports should have been checked
data.owner_user_id = access.token.getUserId(1); data.owner_user_id = access.token.getUserId(1);
if (typeof data.meta === "undefined") { if (typeof data.meta === 'undefined') {
data.meta = {}; data.meta = {};
} }
// streams aren't routed by domain name so don't store domain names in the DB return streamModel
const data_no_domains = structuredClone(data); .query()
delete data_no_domains.domain_names; .insertAndFetch(data)
.then(utils.omitRow(omissions()));
return streamModel.query().insertAndFetch(data_no_domains).then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalStream.update(access, {
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalStream.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
}) })
.then((row) => { .then((row) => {
// Configure nginx // Configure nginx
return internalNginx.configure(streamModel, "stream", row).then(() => { return internalNginx.configure(streamModel, 'stream', row)
return row; .then(() => {
}); return internalStream.get(access, {id: row.id, expand: ['owner']});
});
}) })
.then((row) => { .then((row) => {
// Add to audit log // Add to audit log
return internalAuditLog return internalAuditLog.add(access, {
.add(access, { action: 'created',
action: "created", object_type: 'stream',
object_type: "stream", object_id: row.id,
object_id: row.id, meta: data
meta: data, })
})
.then(() => { .then(() => {
return row; return row;
}); });
@@ -93,78 +59,39 @@ const internalStream = {
* @return {Promise} * @return {Promise}
*/ */
update: (access, data) => { update: (access, data) => {
let thisData = data; return access.can('streams:update', data.id)
const create_certificate = thisData.certificate_id === "new";
if (create_certificate) {
delete thisData.certificate_id;
}
return access
.can("streams:update", thisData.id)
.then((/*access_data*/) => { .then((/*access_data*/) => {
// TODO: at this point the existing streams should have been checked // TODO: at this point the existing streams should have been checked
return internalStream.get(access, { id: thisData.id }); return internalStream.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (row.id !== thisData.id) { if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError( throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
`Stream could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
} }
if (create_certificate) {
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,
},
thisData,
);
return streamModel return streamModel
.query() .query()
.patchAndFetchById(row.id, thisData) .patchAndFetchById(row.id, data)
.then(utils.omitRow(omissions())) .then(utils.omitRow(omissions()))
.then((saved_row) => {
return internalNginx.configure(streamModel, 'stream', saved_row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((saved_row) => { .then((saved_row) => {
// Add to audit log // Add to audit log
return internalAuditLog return internalAuditLog.add(access, {
.add(access, { action: 'updated',
action: "updated", object_type: 'stream',
object_type: "stream", object_id: row.id,
object_id: row.id, meta: data
meta: thisData, })
})
.then(() => { .then(() => {
return saved_row; return saved_row;
}); });
}); });
})
.then(() => {
return internalStream.get(access, { id: thisData.id, expand: ["owner", "certificate"] }).then((row) => {
return internalNginx.configure(streamModel, "stream", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
}); });
}, },
@@ -177,39 +104,38 @@ const internalStream = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
const thisData = data || {}; if (typeof data === 'undefined') {
data = {};
}
return access return access.can('streams:get', data.id)
.can("streams:get", thisData.id)
.then((access_data) => { .then((access_data) => {
const query = streamModel let query = streamModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.andWhere("id", thisData.id) .andWhere('id', data.id)
.allowGraph("[owner,certificate]") .allowGraph('[owner]')
.first(); .first();
if (access_data.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));
} }
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`); query.withGraphFetched('[' + data.expand.join(', ') + ']');
} }
return query.then(utils.omitRow(omissions())); return query.then(utils.omitRow(omissions()));
}) })
.then((row) => { .then((row) => {
let thisRow = row; if (!row) {
if (!thisRow || !thisRow.id) { throw new error.ItemNotFoundError(data.id);
throw new errs.ItemNotFoundError(thisData.id);
} }
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
// Custom omissions // Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
return _.omit(thisRow, thisData.omit); row = _.omit(row, data.omit);
} }
return thisRow; return row;
}); });
}, },
@@ -221,35 +147,35 @@ const internalStream = {
* @returns {Promise} * @returns {Promise}
*/ */
delete: (access, data) => { delete: (access, data) => {
return access return access.can('streams:delete', data.id)
.can("streams:delete", data.id)
.then(() => { .then(() => {
return internalStream.get(access, { id: data.id }); return internalStream.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} }
return streamModel return streamModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
is_deleted: 1, is_deleted: 1
}) })
.then(() => { .then(() => {
// Delete Nginx Config // Delete Nginx Config
return internalNginx.deleteConfig("stream", row).then(() => { return internalNginx.deleteConfig('stream', row)
return internalNginx.reload(); .then(() => {
}); return internalNginx.reload();
});
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "deleted", action: 'deleted',
object_type: "stream", object_type: 'stream',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -266,41 +192,39 @@ const internalStream = {
* @returns {Promise} * @returns {Promise}
*/ */
enable: (access, data) => { enable: (access, data) => {
return access return access.can('streams:update', data.id)
.can("streams:update", data.id)
.then(() => { .then(() => {
return internalStream.get(access, { return internalStream.get(access, {
id: data.id, id: data.id,
expand: ["certificate", "owner"], expand: ['owner']
}); });
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} } else if (row.enabled) {
if (row.enabled) { throw new error.ValidationError('Host is already enabled');
throw new errs.ValidationError("Stream is already enabled");
} }
row.enabled = 1; row.enabled = 1;
return streamModel return streamModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 1, enabled: 1
}) })
.then(() => { .then(() => {
// Configure nginx // Configure nginx
return internalNginx.configure(streamModel, "stream", row); return internalNginx.configure(streamModel, 'stream', row);
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "enabled", action: 'enabled',
object_type: "stream", object_type: 'stream',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -317,40 +241,39 @@ const internalStream = {
* @returns {Promise} * @returns {Promise}
*/ */
disable: (access, data) => { disable: (access, data) => {
return access return access.can('streams:update', data.id)
.can("streams:update", data.id)
.then(() => { .then(() => {
return internalStream.get(access, { id: data.id }); return internalStream.get(access, {id: data.id});
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} } else if (!row.enabled) {
if (!row.enabled) { throw new error.ValidationError('Host is already disabled');
throw new errs.ValidationError("Stream is already disabled");
} }
row.enabled = 0; row.enabled = 0;
return streamModel return streamModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 0, enabled: 0
}) })
.then(() => { .then(() => {
// Delete Nginx Config // Delete Nginx Config
return internalNginx.deleteConfig("stream", row).then(() => { return internalNginx.deleteConfig('stream', row)
return internalNginx.reload(); .then(() => {
}); return internalNginx.reload();
});
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "disabled", action: 'disabled',
object_type: "stream-host", object_type: 'stream-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -368,39 +291,31 @@ const internalStream = {
* @returns {Promise} * @returns {Promise}
*/ */
getAll: (access, expand, search_query) => { getAll: (access, expand, search_query) => {
return access return access.can('streams:list')
.can("streams:list")
.then((access_data) => { .then((access_data) => {
const query = streamModel let query = streamModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.groupBy("id") .groupBy('id')
.allowGraph("[owner,certificate]") .allowGraph('[owner]')
.orderBy("incoming_port", "ASC"); .orderBy('incoming_port', 'ASC');
if (access_data.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 search_query === "string" && search_query.length > 0) { if (typeof search_query === 'string') {
query.where(function () { query.where(function () {
this.where(castJsonIfNeed("incoming_port"), "like", `%${search_query}%`); this.where('incoming_port', 'like', '%' + search_query + '%');
}); });
} }
if (typeof expand !== "undefined" && expand !== null) { if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`); query.withGraphFetched('[' + expand.join(', ') + ']');
} }
return query.then(utils.omitRows(omissions())); return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
}); });
}, },
@@ -412,16 +327,20 @@ const internalStream = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: (user_id, visibility) => { getCount: (user_id, visibility) => {
const query = streamModel.query().count("id AS count").where("is_deleted", 0); let query = streamModel
.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);
} }
return query.first().then((row) => { return query.first()
return Number.parseInt(row.count, 10); .then((row) => {
}); return parseInt(row.count, 10);
}, });
}
}; };
export default internalStream; module.exports = internalStream;

View File

@@ -1,14 +1,12 @@
import _ from "lodash"; const _ = require('lodash');
import errs from "../lib/error.js"; const error = require('../lib/error');
import { parseDatePeriod } from "../lib/helpers.js"; const userModel = require('../models/user');
import authModel from "../models/auth.js"; const authModel = require('../models/auth');
import TokenModel from "../models/token.js"; const helpers = require('../lib/helpers');
import userModel from "../models/user.js"; const TokenModel = require('../models/token');
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password"; module.exports = {
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
export default {
/** /**
* @param {Object} data * @param {Object} data
* @param {String} data.identity * @param {String} data.identity
@@ -18,66 +16,70 @@ export default {
* @param {String} [issuer] * @param {String} [issuer]
* @returns {Promise} * @returns {Promise}
*/ */
getTokenFromEmail: async (data, issuer) => { getTokenFromEmail: (data, issuer) => {
const Token = TokenModel(); let Token = new 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 (!user) { if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); // The scope requested doesn't exist as a role against the user,
} // you shall not pass.
throw new error.AuthError('Invalid scope: ' + data.scope);
}
const auth = await authModel // Create a moment of the expiry expression
.query() let expiry = helpers.parseDatePeriod(data.expiry);
.where("user_id", "=", user.id) if (expiry === null) {
.where("type", "=", "password") throw new error.AuthError('Invalid expiry time: ' + data.expiry);
.first(); }
if (!auth) { return Token.create({
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); iss: issuer || 'api',
} attrs: {
id: user.id
const valid = await auth.verifyPassword(data.secret); },
if (!valid) { scope: [data.scope],
throw new errs.AuthError( expiresIn: data.expiry
ERROR_MESSAGE_INVALID_AUTH, })
ERROR_MESSAGE_INVALID_AUTH_I18N, .then((signed) => {
); return {
} token: signed.token,
expires: expiry.toISOString()
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. } else {
throw new errs.AuthError(`Invalid scope: ${data.scope}`); throw new error.AuthError('Invalid password');
} }
});
// Create a moment of the expiry expression } else {
const expiry = parseDatePeriod(data.expiry); throw new error.AuthError('No password auth for user');
if (expiry === null) { }
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`); });
} } else {
throw new error.AuthError('No relevant user found');
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,70 +89,74 @@ 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(); let Token = new TokenModel();
const thisData = data || {};
thisData.expiry = thisData.expiry || "1d"; data = data || {};
data.expiry = data.expiry || '1d';
if (access && access.token.getUserId(0)) {
if (access?.token.getUserId(0)) {
// Create a moment of the expiry expression // Create a moment of the expiry expression
const expiry = parseDatePeriod(thisData.expiry); let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) { if (expiry === null) {
throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`); throw new error.AuthError('Invalid expiry time: ' + data.expiry);
} }
const token_attrs = { let token_attrs = {
id: access.token.getUserId(0), id: access.token.getUserId(0)
}; };
// Only admins can request otherwise scoped tokens // Only admins can request otherwise scoped tokens
let scope = access.token.get("scope"); let scope = access.token.get('scope');
if (thisData.scope && access.token.hasScope("admin")) { if (data.scope && access.token.hasScope('admin')) {
scope = [thisData.scope]; scope = [data.scope];
if (thisData.scope === "job-board" || thisData.scope === "worker") { if (data.scope === 'job-board' || data.scope === 'worker') {
token_attrs.id = 0; token_attrs.id = 0;
} }
} }
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: data.expiry
}); })
.then((signed) => {
return { return {
token: signed.token, token: signed.token,
expires: expiry.toISOString(), expires: expiry.toISOString()
}; };
});
} else {
throw new error.AssertionFailedError('Existing token contained invalid user data');
} }
throw new error.AssertionFailedError("Existing token contained invalid user data");
}, },
/** /**
* @param {Object} user * @param {Object} user
* @returns {Promise} * @returns {Promise}
*/ */
getTokenFromUser: async (user) => { getTokenFromUser: (user) => {
const expire = "1d"; const expire = '1d';
const Token = TokenModel(); const Token = new TokenModel();
const expiry = parseDatePeriod(expire); const expiry = helpers.parseDatePeriod(expire);
const signed = await Token.create({ return Token.create({
iss: "api", iss: 'api',
attrs: { attrs: {
id: user.id, id: user.id
}, },
scope: ["user"], scope: ['user'],
expiresIn: expire, expiresIn: expire
}); })
.then((signed) => {
return { return {
token: signed.token, token: signed.token,
expires: expiry.toISOString(), expires: expiry.toISOString(),
user: user, user: user
}; };
}, });
}
}; };

View File

@@ -1,76 +1,93 @@
import gravatar from "gravatar"; const _ = require('lodash');
import _ from "lodash"; const error = require('../lib/error');
import errs from "../lib/error.js"; const utils = require('../lib/utils');
import utils from "../lib/utils.js"; const userModel = require('../models/user');
import authModel from "../models/auth.js"; const userPermissionModel = require('../models/user_permission');
import userModel from "../models/user.js"; const authModel = require('../models/auth');
import userPermissionModel from "../models/user_permission.js"; const gravatar = require('gravatar');
import internalAuditLog from "./audit-log.js"; const internalToken = require('./token');
import internalToken from "./token.js"; const internalAuditLog = require('./audit-log');
const omissions = () => { function 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 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; let auth = data.auth || null;
delete data.auth; delete data.auth;
data.avatar = data.avatar || ""; data.avatar = data.avatar || '';
data.roles = data.roles || []; data.roles = data.roles || [];
if (typeof data.is_disabled !== "undefined") { if (typeof data.is_disabled !== 'undefined') {
data.is_disabled = data.is_disabled ? 1 : 0; data.is_disabled = data.is_disabled ? 1 : 0;
} }
await access.can("users:create", data); return access.can('users:create', data)
data.avatar = gravatar.url(data.email, { default: "mm" }); .then(() => {
data.avatar = gravatar.url(data.email, {default: 'mm'});
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); return userModel
if (auth) { .query()
user = await authModel.query().insert({ .insertAndFetch(data)
user_id: user.id, .then(utils.omitRow(omissions()));
type: auth.type, })
secret: auth.secret, .then((user) => {
meta: {}, if (auth) {
return authModel
.query()
.insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
meta: {}
})
.then(() => {
return user;
});
} else {
return user;
}
})
.then((user) => {
// Create permissions row as well
let is_admin = data.roles.indexOf('admin') !== -1;
return userPermissionModel
.query()
.insert({
user_id: user.id,
visibility: is_admin ? 'all' : 'user',
proxy_hosts: 'manage',
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;
}, },
/** /**
@@ -82,57 +99,62 @@ const internalUser = {
* @return {Promise} * @return {Promise}
*/ */
update: (access, data) => { update: (access, data) => {
if (typeof data.is_disabled !== "undefined") { if (typeof data.is_disabled !== 'undefined') {
data.is_disabled = data.is_disabled ? 1 : 0; data.is_disabled = data.is_disabled ? 1 : 0;
} }
return access return access.can('users:update', data.id)
.can("users:update", data.id)
.then(() => { .then(() => {
// Make sure that the user being updated doesn't change their email to another user that is already using it // Make sure that the user being updated doesn't change their email to another user that is already using it
// 1. get user we want to update // 1. get user we want to update
return internalUser.get(access, { id: data.id }).then((user) => { return internalUser.get(access, {id: data.id})
// 2. if email is to be changed, find other users with that email .then((user) => {
if (typeof data.email !== "undefined") {
data.email = data.email.toLowerCase().trim();
if (user.email !== data.email) { // 2. if email is to be changed, find other users with that email
return internalUser.isEmailAvailable(data.email, data.id).then((available) => { if (typeof data.email !== 'undefined') {
if (!available) { data.email = data.email.toLowerCase().trim();
throw new errs.ValidationError(`Email address already in use - ${data.email}`);
} if (user.email !== data.email) {
return user; return internalUser.isEmailAvailable(data.email, data.id)
}); .then((available) => {
if (!available) {
throw new error.ValidationError('Email address already in use - ' + data.email);
}
return user;
});
}
} }
}
// No change to email: // No change to email:
return user; return user;
}); });
}) })
.then((user) => { .then((user) => {
if (user.id !== data.id) { if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError( throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
} }
data.avatar = gravatar.url(data.email || user.email, { default: "mm" }); data.avatar = gravatar.url(data.email || user.email, {default: 'mm'});
return userModel.query().patchAndFetchById(user.id, data).then(utils.omitRow(omissions()));
return userModel
.query()
.patchAndFetchById(user.id, data)
.then(utils.omitRow(omissions()));
}) })
.then(() => { .then(() => {
return internalUser.get(access, { id: data.id }); return internalUser.get(access, {id: data.id});
}) })
.then((user) => { .then((user) => {
// Add to audit log // Add to audit log
return internalAuditLog return internalAuditLog.add(access, {
.add(access, { action: 'updated',
action: "updated", object_type: 'user',
object_type: "user", object_id: user.id,
object_id: user.id, meta: data
meta: data, })
})
.then(() => { .then(() => {
return user; return user;
}); });
@@ -148,41 +170,37 @@ const internalUser = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
const thisData = data || {}; if (typeof data === 'undefined') {
data = {};
if (typeof thisData.id === "undefined" || !thisData.id) {
thisData.id = access.token.getUserId(0);
} }
return access if (typeof data.id === 'undefined' || !data.id) {
.can("users:get", thisData.id) data.id = access.token.getUserId(0);
}
return access.can('users:get', data.id)
.then(() => { .then(() => {
const query = userModel let query = userModel
.query() .query()
.where("is_deleted", 0) .where('is_deleted', 0)
.andWhere("id", thisData.id) .andWhere('id', data.id)
.allowGraph("[permissions]") .allowGraph('[permissions]')
.first(); .first();
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`); query.withGraphFetched('[' + data.expand.join(', ') + ']');
} }
return query.then(utils.omitRow(omissions())); return query.then(utils.omitRow(omissions()));
}) })
.then((row) => { .then((row) => {
if (!row || !row.id) { if (!row) {
throw new errs.ItemNotFoundError(thisData.id); throw new error.ItemNotFoundError(data.id);
} }
// Custom omissions // Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { if (typeof data.omit !== 'undefined' && data.omit !== null) {
return _.omit(row, thisData.omit); row = _.omit(row, data.omit);
} }
if (row.avatar === "") {
row.avatar = DEFAULT_AVATAR;
}
return row; return row;
}); });
}, },
@@ -195,15 +213,20 @@ const internalUser = {
* @param user_id * @param user_id
*/ */
isEmailAvailable: (email, user_id) => { isEmailAvailable: (email, user_id) => {
const query = userModel.query().where("email", "=", email.toLowerCase().trim()).where("is_deleted", 0).first(); let query = userModel
.query()
.where('email', '=', email.toLowerCase().trim())
.where('is_deleted', 0)
.first();
if (typeof user_id !== "undefined") { if (typeof user_id !== 'undefined') {
query.where("id", "!=", user_id); query.where('id', '!=', user_id);
} }
return query.then((user) => { return query
return !user; .then((user) => {
}); return !user;
});
}, },
/** /**
@@ -214,34 +237,33 @@ const internalUser = {
* @returns {Promise} * @returns {Promise}
*/ */
delete: (access, data) => { delete: (access, data) => {
return access return access.can('users:delete', data.id)
.can("users:delete", data.id)
.then(() => { .then(() => {
return internalUser.get(access, { id: data.id }); return internalUser.get(access, {id: data.id});
}) })
.then((user) => { .then((user) => {
if (!user) { if (!user) {
throw new errs.ItemNotFoundError(data.id); throw new error.ItemNotFoundError(data.id);
} }
// Make sure user can't delete themselves // Make sure user can't delete themselves
if (user.id === access.token.getUserId(0)) { if (user.id === access.token.getUserId(0)) {
throw new errs.PermissionError("You cannot delete yourself."); throw new error.PermissionError('You cannot delete yourself.');
} }
return userModel return userModel
.query() .query()
.where("id", user.id) .where('id', user.id)
.patch({ .patch({
is_deleted: 1, is_deleted: 1
}) })
.then(() => { .then(() => {
// Add to audit log // Add to audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "deleted", action: 'deleted',
object_type: "user", object_type: 'user',
object_id: user.id, object_id: user.id,
meta: _.omit(user, omissions()), meta: _.omit(user, omissions())
}); });
}); });
}) })
@@ -250,14 +272,6 @@ const internalUser = {
}); });
}, },
deleteAll: async () => {
await userModel
.query()
.patch({
is_deleted: 1,
});
},
/** /**
* This will only count the users * This will only count the users
* *
@@ -266,26 +280,26 @@ const internalUser = {
* @returns {*} * @returns {*}
*/ */
getCount: (access, search_query) => { getCount: (access, search_query) => {
return access return access.can('users:list')
.can("users:list")
.then(() => { .then(() => {
const query = userModel.query().count("id as count").where("is_deleted", 0).first(); let query = userModel
.query()
.count('id as count')
.where('is_deleted', 0)
.first();
// 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("user.name", "like", `%${search_query}%`).orWhere( this.where('user.name', 'like', '%' + search_query + '%')
"user.email", .orWhere('user.email', 'like', '%' + search_query + '%');
"like",
`%${search_query}%`,
);
}); });
} }
return query; return query;
}) })
.then((row) => { .then((row) => {
return Number.parseInt(row.count, 10); return parseInt(row.count, 10);
}); });
}, },
@@ -297,28 +311,30 @@ const internalUser = {
* @param {String} [search_query] * @param {String} [search_query]
* @returns {Promise} * @returns {Promise}
*/ */
getAll: async (access, expand, search_query) => { getAll: (access, expand, search_query) => {
await access.can("users:list"); return access.can('users:list')
const query = userModel .then(() => {
.query() let query = userModel
.where("is_deleted", 0) .query()
.groupBy("id") .where('is_deleted', 0)
.allowGraph("[permissions]") .groupBy('id')
.orderBy("name", "ASC"); .allowGraph('[permissions]')
.orderBy('name', 'ASC');
// 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 + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
}
return query.then(utils.omitRows(omissions()));
}); });
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const res = await query;
return utils.omitRows(omissions())(res);
}, },
/** /**
@@ -326,11 +342,11 @@ const internalUser = {
* @param {Integer} [id_requested] * @param {Integer} [id_requested]
* @returns {[String]} * @returns {[String]}
*/ */
getUserOmisionsByAccess: (access, idRequested) => { getUserOmisionsByAccess: (access, id_requested) => {
let response = []; // Admin response let response = []; // Admin response
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) { if (!access.token.hasScope('admin') && access.token.getUserId(0) !== id_requested) {
response = ["is_deleted"]; // Restricted response response = ['roles', 'is_deleted']; // Restricted response
} }
return response; return response;
@@ -345,30 +361,26 @@ const internalUser = {
* @return {Promise} * @return {Promise}
*/ */
setPassword: (access, data) => { setPassword: (access, data) => {
return access return access.can('users:password', data.id)
.can("users:password", data.id)
.then(() => { .then(() => {
return internalUser.get(access, { id: data.id }); return internalUser.get(access, {id: data.id});
}) })
.then((user) => { .then((user) => {
if (user.id !== data.id) { if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError( throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
} }
if (user.id === access.token.getUserId(0)) { if (user.id === access.token.getUserId(0)) {
// they're setting their own password. Make sure their current password is correct // they're setting their own password. Make sure their current password is correct
if (typeof data.current === "undefined" || !data.current) { if (typeof data.current === 'undefined' || !data.current) {
throw new errs.ValidationError("Current password was not supplied"); throw new error.ValidationError('Current password was not supplied');
} }
return internalToken return internalToken.getTokenFromEmail({
.getTokenFromEmail({ identity: user.email,
identity: user.email, secret: data.current
secret: data.current, })
})
.then(() => { .then(() => {
return user; return user;
}); });
@@ -380,36 +392,43 @@ const internalUser = {
// Get auth, patch if it exists // Get auth, patch if it exists
return authModel return authModel
.query() .query()
.where("user_id", user.id) .where('user_id', user.id)
.andWhere("type", data.type) .andWhere('type', data.type)
.first() .first()
.then((existing_auth) => { .then((existing_auth) => {
if (existing_auth) { if (existing_auth) {
// patch // patch
return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({ return authModel
type: data.type, // This is required for the model to encrypt on save .query()
secret: data.secret, .where('user_id', user.id)
}); .andWhere('type', data.type)
.patch({
type: data.type, // This is required for the model to encrypt on save
secret: data.secret
});
} else {
// insert
return authModel
.query()
.insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {}
});
} }
// insert
return authModel.query().insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {},
});
}) })
.then(() => { .then(() => {
// Add to Audit Log // Add to Audit Log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "updated", action: 'updated',
object_type: "user", object_type: 'user',
object_id: user.id, object_id: user.id,
meta: { meta: {
name: user.name, name: user.name,
password_changed: true, password_changed: true,
auth_type: data.type, auth_type: data.type
}, }
}); });
}); });
}) })
@@ -424,17 +443,14 @@ const internalUser = {
* @return {Promise} * @return {Promise}
*/ */
setPermissions: (access, data) => { setPermissions: (access, data) => {
return access return access.can('users:permissions', data.id)
.can("users:permissions", data.id)
.then(() => { .then(() => {
return internalUser.get(access, { id: data.id }); return internalUser.get(access, {id: data.id});
}) })
.then((user) => { .then((user) => {
if (user.id !== data.id) { if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened // Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError( throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
} }
return user; return user;
@@ -443,30 +459,34 @@ const internalUser = {
// Get perms row, patch if it exists // Get perms row, patch if it exists
return userPermissionModel return userPermissionModel
.query() .query()
.where("user_id", user.id) .where('user_id', user.id)
.first() .first()
.then((existing_auth) => { .then((existing_auth) => {
if (existing_auth) { if (existing_auth) {
// patch // patch
return userPermissionModel return userPermissionModel
.query() .query()
.where("user_id", user.id) .where('user_id', user.id)
.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data)); .patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data));
} else {
// insert
return userPermissionModel
.query()
.insertAndFetch(_.assign({user_id: user.id}, data));
} }
// insert
return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data));
}) })
.then((permissions) => { .then((permissions) => {
// Add to Audit Log // Add to Audit Log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
action: "updated", action: 'updated',
object_type: "user", object_type: 'user',
object_id: user.id, object_id: user.id,
meta: { meta: {
name: user.name, name: user.name,
permissions: permissions, permissions: permissions
}, }
}); });
}); });
}) })
.then(() => { .then(() => {
@@ -480,15 +500,14 @@ const internalUser = {
* @param {Integer} data.id * @param {Integer} data.id
*/ */
loginAs: (access, data) => { loginAs: (access, data) => {
return access return access.can('users:loginas', data.id)
.can("users:loginas", data.id)
.then(() => { .then(() => {
return internalUser.get(access, data); return internalUser.get(access, data);
}) })
.then((user) => { .then((user) => {
return internalToken.getTokenFromUser(user); return internalToken.getTokenFromUser(user);
}); });
}, }
}; };
export default internalUser; module.exports = internalUser;

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
development: { development: {
client: 'mysql2', client: 'mysql',
migrations: { migrations: {
tableName: 'migrations', tableName: 'migrations',
stub: 'lib/migrate_template.js', stub: 'lib/migrate_template.js',
@@ -9,7 +9,7 @@ module.exports = {
}, },
production: { production: {
client: 'mysql2', client: 'mysql',
migrations: { migrations: {
tableName: 'migrations', tableName: 'migrations',
stub: 'lib/migrate_template.js', stub: 'lib/migrate_template.js',

View File

@@ -4,90 +4,91 @@
* "scope" in this file means "where did this token come from and what is using it", so 99% of the time * "scope" in this file means "where did this token come from and what is using it", so 99% of the time
* the "scope" is going to be "user" because it would be a user token. This is not to be confused with * the "scope" is going to be "user" because it would be a user token. This is not to be confused with
* the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else. * the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else.
*
*
*/ */
import fs from "node:fs"; const _ = require('lodash');
import { dirname } from "node:path"; const logger = require('../logger').access;
import { fileURLToPath } from "node:url"; const validator = require('ajv');
import Ajv from "ajv/dist/2020.js"; const error = require('./error');
import _ from "lodash"; const userModel = require('../models/user');
import { access as logger } from "../logger.js"; const proxyHostModel = require('../models/proxy_host');
import proxyHostModel from "../models/proxy_host.js"; const TokenModel = require('../models/token');
import TokenModel from "../models/token.js"; const roleSchema = require('./access/roles.json');
import userModel from "../models/user.js"; const permsSchema = require('./access/permissions.json');
import permsSchema from "./access/permissions.json" with { type: "json" };
import roleSchema from "./access/roles.json" with { type: "json" };
import errs from "./error.js";
const __filename = fileURLToPath(import.meta.url); module.exports = function (token_string) {
const __dirname = dirname(__filename); let Token = new TokenModel();
let token_data = null;
export default function (tokenString) { let initialised = false;
const Token = TokenModel(); let object_cache = {};
let tokenData = null; let allow_internal_access = false;
let initialised = false; let user_roles = [];
const objectCache = {}; let permissions = {};
let allowInternalAccess = false;
let userRoles = [];
let permissions = {};
/** /**
* Loads the Token object from the token string * Loads the Token object from the token string
* *
* @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 error.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 error.AuthError('Invalid token scope for User');
} else {
initialised = true;
user_roles = user.roles;
permissions = user.permissions;
}
} else {
throw new error.AuthError('User cannot be loaded for Token');
}
});
} else {
initialised = true;
}
}));
} }
} });
initialised = true;
}; };
/** /**
@@ -95,121 +96,141 @@ 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 error.AuthError('User Token supplied without a User ID'));
} else {
let 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");
}
const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0; // USERS - should only return yourself
case 'users':
resolve(token_user_id ? [token_user_id] : []);
break;
if (typeof objectCache[objectType] !== "undefined") { // Proxy Hosts
objects = objectCache[objectType]; case 'proxy_hosts':
} else { query = proxyHostModel
switch (objectType) { .query()
// USERS - should only return yourself .select('id')
case "users": .andWhere('is_deleted', 0);
objects = tokenUserId ? [tokenUserId] : [];
break;
// Proxy Hosts if (permissions.visibility === 'user') {
case "proxy_hosts": { query.andWhere('owner_user_id', token_user_id);
const query = proxyHostModel }
.query()
.select("id")
.andWhere("is_deleted", 0);
if (permissions.visibility === "user") { resolve(query
query.andWhere("owner_user_id", tokenUserId); .then((rows) => {
let result = [];
_.forEach(rows, (rule_row) => {
result.push(rule_row.id);
});
// enum should not have less than 1 item
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) => {
result.push(ruleRow.id);
});
// enum should not have less than 1 item
if (!objects.length) {
objects.push(0);
}
break;
} }
} }
objectCache[objectType] = objects; } else {
resolve(null);
} }
} })
return objects; .then((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(); let base_object_type = permission_label.split(':').shift();
const schema = { let schema = {
$id: "objects", $id: 'objects',
description: "Actor Properties", $schema: 'http://json-schema.org/draft-07/schema#',
type: "object", description: 'Actor Properties',
type: 'object',
additionalProperties: false, additionalProperties: false,
properties: { properties: {
user_id: { user_id: {
anyOf: [ anyOf: [
{ {
type: "number", type: 'number',
enum: [Token.get("attrs").id], enum: [Token.get('attrs').id]
}, }
], ]
}, },
scope: { scope: {
type: "string", type: 'string',
pattern: `^${Token.get("scope")}$`, pattern: '^' + Token.get('scope') + '$'
}, }
}, }
}; };
const result = await this.loadObjects(baseObjectType); return this.loadObjects(base_object_type)
if (typeof result === "object" && result !== null) { .then((object_result) => {
schema.properties[baseObjectType] = { if (typeof object_result === 'object' && object_result !== null) {
type: "number", schema.properties[base_object_type] = {
enum: result, type: 'number',
minimum: 1, enum: object_result,
}; minimum: 1
} else { };
schema.properties[baseObjectType] = { } else {
type: "number", schema.properties[base_object_type] = {
minimum: 1, type: 'number',
}; 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(function (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,
@@ -220,59 +241,74 @@ export default function (tokenString) {
* @param {*} [data] * @param {*} [data]
* @returns {Promise} * @returns {Promise}
*/ */
can: async (permission, data) => { can: (permission, data) => {
if (allowInternalAccess === true) { if (allow_internal_access === true) {
return true; return Promise.resolve(true);
//return true;
} else {
return this.init()
.then(() => {
// Initialised, token decoded ok
return this.getObjectSchema(permission)
.then((objectSchema) => {
let data_schema = {
[permission]: {
data: data,
scope: Token.get('scope'),
roles: user_roles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates
}
};
let permissionSchema = {
$schema: 'http://json-schema.org/draft-07/schema#',
$async: true,
$id: 'permissions',
additionalProperties: false,
properties: {}
};
permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
// logger.info('objectSchema', JSON.stringify(objectSchema, null, 2));
// logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2));
// logger.info('data_schema', JSON.stringify(data_schema, null, 2));
let ajv = validator({
verbose: true,
allErrors: true,
format: 'full',
missingRefs: 'fail',
breakOnError: true,
coerceTypes: true,
schemas: [
roleSchema,
permsSchema,
objectSchema,
permissionSchema
]
});
return ajv.validate('permissions', data_schema)
.then(() => {
return data_schema[permission];
});
});
})
.catch((err) => {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw new error.PermissionError('Permission Denied', err);
});
} }
}
try {
await this.init();
const objectSchema = await this.getObjectSchema(permission);
const dataSchema = {
[permission]: {
data: data,
scope: Token.get("scope"),
roles: userRoles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates,
},
};
const permissionSchema = {
$async: true,
$id: "permissions",
type: "object",
additionalProperties: false,
properties: {},
};
const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, {
encoding: "utf8",
});
permissionSchema.properties[permission] = JSON.parse(rawData);
const ajv = new Ajv({
verbose: true,
allErrors: true,
breakOnError: true,
coerceTypes: true,
schemas: [roleSchema, permsSchema, objectSchema, permissionSchema],
});
const valid = ajv.validate("permissions", dataSchema);
return valid && dataSchema[permission];
} catch (err) {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw errs.PermissionError("Permission Denied", err);
}
},
}; };
} };

View File

@@ -1,4 +1,5 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "perms", "$id": "perms",
"definitions": { "definitions": {
"view": { "view": {

View File

@@ -1,4 +1,5 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "roles", "$id": "roles",
"definitions": { "definitions": {
"admin": { "admin": {

View File

@@ -1,87 +1,78 @@
import batchflow from "batchflow"; const dnsPlugins = require('../global/certbot-dns-plugins.json');
import dnsPlugins from "../global/certbot-dns-plugins.json" with { type: "json" }; const utils = require('./utils');
import { certbot as logger } from "../logger.js"; const error = require('./error');
import errs from "./error.js"; const logger = require('../logger').certbot;
import utils from "./utils.js"; const batchflow = require('batchflow');
const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')"; const CERTBOT_VERSION_REPLACEMENT = '$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')';
/** const certbot = {
* @param {array} pluginKeys
*/
const installPlugins = async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => { /**
if (pluginKeys.length === 0) { * @param {array} pluginKeys
resolve(); */
return; installPlugins: async function (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 error.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
* ../global/certbot-dns-plugins.json
*
* @param {string} pluginKey
* @returns {Object}
*/
installPlugin: async function (pluginKey) {
if (typeof dnsPlugins[pluginKey] === 'undefined') {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new error.ItemNotFoundError(pluginKey);
} }
batchflow(pluginKeys) const plugin = dnsPlugins[pluginKey];
.sequential() logger.start(`Installing ${pluginKey}...`);
.each((_i, pluginKey, next) => {
certbot plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
.installPlugin(pluginKey) plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
.then(() => {
next(); const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate';
}) return utils.exec(cmd)
.catch((err) => { .then((result) => {
hasErrors = true; logger.complete(`Installed ${pluginKey}`);
next(err); return result;
});
}) })
.error((err) => { .catch((err) => {
logger.error(err.message); throw err;
})
.end(() => {
if (hasErrors) {
reject(
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
);
} else {
resolve();
}
}); });
}); },
}; };
/** module.exports = certbot;
* Installs a cerbot plugin given the key for the object from
* ../global/certbot-dns-plugins.json
*
* @param {string} pluginKey
* @returns {Object}
*/
const installPlugin = async (pluginKey) => {
if (typeof dnsPlugins[pluginKey] === "undefined") {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new errs.ItemNotFoundError(pluginKey);
}
const plugin = dnsPlugins[pluginKey];
logger.start(`Installing ${pluginKey}...`);
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
// SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly
// in new versions of Python
let env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: "stdlib" });
if (typeof plugin.env === "object") {
env = Object.assign(env, plugin.env);
}
const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`;
return utils
.exec(cmd, { env })
.then((result) => {
logger.complete(`Installed ${pluginKey}`);
return result;
})
.catch((err) => {
throw err;
});
};
export { installPlugins, installPlugin };

View File

@@ -1,31 +1,26 @@
import fs from "node:fs"; const fs = require('fs');
import NodeRSA from "node-rsa"; const NodeRSA = require('node-rsa');
import { global as logger } from "../logger.js"; const logger = require('../logger').global;
const keysFile = '/data/keys.json'; const keysFile = '/data/keys.json';
const mysqlEngine = 'mysql2';
const postgresEngine = 'pg';
const sqliteClientName = 'sqlite3';
let instance = null; let instance = null;
// 1. Load from config file first (not recommended anymore) // 1. Load from config file first (not recommended anymore)
// 2. Use config env variables next // 2. Use config env variables next
const configure = () => { const configure = () => {
const filename = `${process.env.NODE_CONFIG_DIR || "./config"}/${process.env.NODE_ENV || "default"}.json`; const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json';
if (fs.existsSync(filename)) { if (fs.existsSync(filename)) {
let configData; let configData;
try { try {
// Load this json synchronously configData = require(filename);
const rawData = fs.readFileSync(filename); } catch (err) {
configData = JSON.parse(rawData);
} catch (_) {
// do nothing // do nothing
} }
if (configData?.database) { if (configData && configData.database) {
logger.info(`Using configuration from file: ${filename}`); logger.info(`Using configuration from file: ${filename}`);
instance = configData; instance = configData;
instance.keys = getKeys(); instance.keys = getKeys();
return; return;
} }
@@ -36,53 +31,33 @@ const configure = () => {
const envMysqlName = process.env.DB_MYSQL_NAME || null; const envMysqlName = process.env.DB_MYSQL_NAME || null;
if (envMysqlHost && envMysqlUser && envMysqlName) { if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql // we have enough mysql creds to go with mysql
logger.info("Using MySQL configuration"); logger.info('Using MySQL configuration');
instance = { instance = {
database: { database: {
engine: mysqlEngine, engine: 'mysql',
host: envMysqlHost, host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306, port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser, user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD, password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName, name: envMysqlName,
}, },
keys: getKeys(), keys: getKeys(),
}; };
return; return;
} }
const envPostgresHost = process.env.DB_POSTGRES_HOST || null; const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/database.sqlite';
const envPostgresUser = process.env.DB_POSTGRES_USER || null;
const envPostgresName = process.env.DB_POSTGRES_NAME || null;
if (envPostgresHost && envPostgresUser && envPostgresName) {
// we have enough postgres creds to go with postgres
logger.info("Using Postgres configuration");
instance = {
database: {
engine: postgresEngine,
host: envPostgresHost,
port: process.env.DB_POSTGRES_PORT || 5432,
user: envPostgresUser,
password: process.env.DB_POSTGRES_PASSWORD,
name: envPostgresName,
},
keys: getKeys(),
};
return;
}
const envSqliteFile = process.env.DB_SQLITE_FILE || "/data/database.sqlite";
logger.info(`Using Sqlite: ${envSqliteFile}`); logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = { instance = {
database: { database: {
engine: "knex-native", engine: 'knex-native',
knex: { knex: {
client: sqliteClientName, client: 'sqlite3',
connection: { connection: {
filename: envSqliteFile, filename: envSqliteFile
}, },
useNullAsDefault: true, useNullAsDefault: true
}, }
}, },
keys: getKeys(), keys: getKeys(),
}; };
@@ -90,155 +65,120 @@ const configure = () => {
const getKeys = () => { const getKeys = () => {
// Get keys from file // Get keys from file
logger.debug("Cheecking for keys file:", keysFile);
if (!fs.existsSync(keysFile)) { if (!fs.existsSync(keysFile)) {
generateKeys(); generateKeys();
} else if (process.env.DEBUG) { } else if (process.env.DEBUG) {
logger.info("Keys file exists OK"); logger.info('Keys file exists OK');
} }
try { try {
// Load this json keysFile synchronously and return the json object return require(keysFile);
const rawData = fs.readFileSync(keysFile);
return JSON.parse(rawData);
} catch (err) { } catch (err) {
logger.error(`Could not read JWT key pair from config file: ${keysFile}`, err); logger.error('Could not read JWT key pair from config file: ' + keysFile, err);
process.exit(1); process.exit(1);
} }
}; };
const generateKeys = () => { const generateKeys = () => {
logger.info("Creating a new JWT key pair..."); logger.info('Creating a new JWT key pair...');
// Now create the keys and save them in the config. // Now create the keys and save them in the config.
const key = new NodeRSA({ b: 2048 }); const key = new NodeRSA({ b: 2048 });
key.generateKeyPair(); key.generateKeyPair();
const keys = { const keys = {
key: key.exportKey("private").toString(), key: key.exportKey('private').toString(),
pub: key.exportKey("public").toString(), pub: key.exportKey('public').toString(),
}; };
// Write keys config // Write keys config
try { try {
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2)); fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2));
} catch (err) { } catch (err) {
logger.error(`Could not write JWT key pair to config file: ${keysFile}: ${err.message}`); logger.error('Could not write JWT key pair to config file: ' + keysFile + ': ' + err.message);
process.exit(1); process.exit(1);
} }
logger.info(`Wrote JWT key pair to config file: ${keysFile}`); logger.info('Wrote JWT key pair to config file: ' + keysFile);
}; };
/** module.exports = {
*
* @param {string} key ie: 'database' or 'database.engine' /**
* @returns {boolean} *
*/ * @param {string} key ie: 'database' or 'database.engine'
const configHas = (key) => { * @returns {boolean}
instance === null && configure(); */
const keys = key.split("."); has: function(key) {
let level = instance; instance === null && configure();
let has = true; const keys = key.split('.');
keys.forEach((keyItem) => { let level = instance;
if (typeof level[keyItem] === "undefined") { let has = true;
has = false; keys.forEach((keyItem) =>{
} else { if (typeof level[keyItem] === 'undefined') {
level = level[keyItem]; has = false;
} else {
level = level[keyItem];
}
});
return has;
},
/**
* Gets a specific key from the top level
*
* @param {string} key
* @returns {*}
*/
get: function (key) {
instance === null && configure();
if (key && typeof instance[key] !== 'undefined') {
return instance[key];
} }
}); return instance;
},
return has; /**
}; * Is this a sqlite configuration?
*
* @returns {boolean}
*/
isSqlite: function () {
instance === null && configure();
return instance.database.knex && instance.database.knex.client === 'sqlite3';
},
/** /**
* Gets a specific key from the top level * Are we running in debug mdoe?
* *
* @param {string} key * @returns {boolean}
* @returns {*} */
*/ debug: function () {
const configGet = (key) => { return !!process.env.DEBUG;
instance === null && configure(); },
if (key && typeof instance[key] !== "undefined") {
return instance[key]; /**
* Returns a public key
*
* @returns {string}
*/
getPublicKey: function () {
instance === null && configure();
return instance.keys.pub;
},
/**
* Returns a private key
*
* @returns {string}
*/
getPrivateKey: function () {
instance === null && configure();
return instance.keys.key;
},
/**
* @returns {boolean}
*/
useLetsencryptStaging: function () {
return !!process.env.LE_STAGING;
} }
return instance;
}; };
/**
* Is this a sqlite configuration?
*
* @returns {boolean}
*/
const isSqlite = () => {
instance === null && configure();
return instance.database.knex && instance.database.knex.client === sqliteClientName;
};
/**
* Is this a mysql configuration?
*
* @returns {boolean}
*/
const isMysql = () => {
instance === null && configure();
return instance.database.engine === mysqlEngine;
};
/**
* Is this a postgres configuration?
*
* @returns {boolean}
*/
const isPostgres = () => {
instance === null && configure();
return instance.database.engine === postgresEngine;
};
/**
* Are we running in debug mdoe?
*
* @returns {boolean}
*/
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 {string}
*/
const getPublicKey = () => {
instance === null && configure();
return instance.keys.pub;
};
/**
* Returns a private key
*
* @returns {string}
*/
const getPrivateKey = () => {
instance === null && configure();
return instance.keys.key;
};
/**
* @returns {boolean}
*/
const useLetsencryptStaging = () => !!process.env.LE_STAGING;
/**
* @returns {string|null}
*/
const useLetsencryptServer = () => {
if (process.env.LE_SERVER) {
return process.env.LE_SERVER;
}
return null;
};
export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer };

View File

@@ -1,103 +1,99 @@
import _ from "lodash"; const _ = require('lodash');
const util = require('util');
const errs = { module.exports = {
PermissionError: function (_, previous) {
PermissionError: function (message, previous) {
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 = "Permission Denied"; this.message = 'Permission Denied';
this.public = true; this.public = true;
this.status = 403; this.status = 403;
}, },
ItemNotFoundError: function (id, previous) { ItemNotFoundError: function (id, previous) {
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.public = true;
this.message = `Not Found - ${id}`; this.status = 404;
}
this.public = true;
this.status = 404;
}, },
AuthError: function (message, messageI18n, previous) { AuthError: function (message, previous) {
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 = message; this.message = message;
this.message_i18n = messageI18n; this.public = true;
this.public = true; this.status = 401;
this.status = 400;
}, },
InternalError: function (message, previous) { InternalError: function (message, previous) {
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 = message; this.message = message;
this.status = 500; this.status = 500;
this.public = false; this.public = false;
}, },
InternalValidationError: function (message, previous) { InternalValidationError: function (message, previous) {
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 = message; this.message = message;
this.status = 400; this.status = 400;
this.public = false; this.public = false;
}, },
ConfigurationError: function (message, previous) { ConfigurationError: function (message, previous) {
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 = message; this.message = message;
this.status = 400; this.status = 400;
this.public = true; this.public = true;
}, },
CacheError: function (message, previous) { CacheError: function (message, previous) {
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name; this.name = this.constructor.name;
this.message = message; this.message = message;
this.previous = previous; this.previous = previous;
this.status = 500; this.status = 500;
this.public = false; this.public = false;
}, },
ValidationError: function (message, previous) { ValidationError: function (message, previous) {
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 = message; this.message = message;
this.public = true; this.public = true;
this.status = 400; this.status = 400;
}, },
AssertionFailedError: function (message, previous) { AssertionFailedError: function (message, previous) {
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 = message; this.message = message;
this.public = false; this.public = false;
this.status = 400; this.status = 400;
}, },
CommandError: function (stdErr, code, previous) { CommandError: function (stdErr, code, previous) {
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 = stdErr; this.message = stdErr;
this.code = code; this.code = code;
this.public = false; this.public = false;
}, },
}; };
_.forEach(errs, (err) => { _.forEach(module.exports, function (error) {
err.prototype = Object.create(Error.prototype); util.inherits(error, Error);
}); });
export default errs;

View File

@@ -1,17 +1,40 @@
export default (req, res, next) => { const validator = require('../validator');
module.exports = function (req, res, next) {
if (req.headers.origin) { if (req.headers.origin) {
res.set({
"Access-Control-Allow-Origin": req.headers.origin, const originSchema = {
"Access-Control-Allow-Credentials": true, oneOf: [
"Access-Control-Allow-Methods": "OPTIONS, GET, POST", {
"Access-Control-Allow-Headers": type: 'string',
"Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit", pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$'
"Access-Control-Max-Age": 5 * 60, },
"Access-Control-Expose-Headers": "X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit", {
}); type: 'string',
next(); pattern: '^[a-z\\-]+:\\/\\/(?:\\[([a-z0-9]{0,4}\\:?)+\\])?/?(:[0-9]+)?$'
}
]
};
// very relaxed validation....
validator(originSchema, req.headers.origin)
.then(function () {
res.set({
'Access-Control-Allow-Origin': req.headers.origin,
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit',
'Access-Control-Max-Age': 5 * 60,
'Access-Control-Expose-Headers': 'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
});
next();
})
.catch(next);
} else { } else {
// No origin // No origin
next(); next();
} }
}; };

View File

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

View File

@@ -1,13 +1,13 @@
export default function () { module.exports = function () {
return (req, res, next) => { return function (req, res, next) {
if (req.headers.authorization) { if (req.headers.authorization) {
const parts = req.headers.authorization.split(" "); let parts = req.headers.authorization.split(' ');
if (parts && parts[0] === "Bearer" && parts[1]) { if (parts && parts[0] === 'Bearer' && parts[1]) {
res.locals.token = parts[1]; res.locals.token = parts[1];
} }
} }
next(); next();
}; };
} };

View File

@@ -1,6 +1,7 @@
import _ from "lodash"; let _ = require('lodash');
module.exports = function (default_sort, default_offset, default_limit, max_limit) {
export default (default_sort, default_offset, default_limit, max_limit) => {
/** /**
* This will setup the req query params with filtered data and defaults * This will setup the req query params with filtered data and defaults
* *
@@ -10,35 +11,34 @@ export default (default_sort, default_offset, default_limit, max_limit) => {
* *
*/ */
return (req, _res, next) => { return function (req, res, next) {
req.query.offset =
typeof req.query.limit === "undefined" ? default_offset || 0 : Number.parseInt(req.query.offset, 10); req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10);
req.query.limit = req.query.limit = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10);
typeof req.query.limit === "undefined" ? default_limit || 50 : Number.parseInt(req.query.limit, 10);
if (max_limit && req.query.limit > max_limit) { if (max_limit && req.query.limit > max_limit) {
req.query.limit = max_limit; req.query.limit = max_limit;
} }
// Sorting // Sorting
let sort = typeof req.query.sort === "undefined" ? default_sort : req.query.sort; let sort = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort;
const myRegexp = /.*\.(asc|desc)$/gi; let myRegexp = /.*\.(asc|desc)$/ig;
const sort_array = []; let sort_array = [];
sort = sort.split(","); sort = sort.split(',');
_.map(sort, (val) => { _.map(sort, function (val) {
const matches = myRegexp.exec(val); let matches = myRegexp.exec(val);
if (matches !== null) { if (matches !== null) {
const dir = matches[1]; let dir = matches[1];
sort_array.push({ sort_array.push({
field: val.substr(0, val.length - (dir.length + 1)), field: val.substr(0, val.length - (dir.length + 1)),
dir: dir.toLowerCase(), dir: dir.toLowerCase()
}); });
} else { } else {
sort_array.push({ sort_array.push({
field: val, field: val,
dir: "asc", dir: 'asc'
}); });
} }
}); });

View File

@@ -1,8 +1,9 @@
export default (req, res, next) => { module.exports = (req, res, next) => {
if (req.params.user_id === 'me' && res.locals.access) { if (req.params.user_id === 'me' && res.locals.access) {
req.params.user_id = res.locals.access.token.get('attrs').id; req.params.user_id = res.locals.access.token.get('attrs').id;
} else { } else {
req.params.user_id = Number.parseInt(req.params.user_id, 10); req.params.user_id = parseInt(req.params.user_id, 10);
} }
next(); next();
}; };

View File

@@ -1,58 +1,32 @@
import moment from "moment"; const moment = require('moment');
import { ref } from "objection";
import { isPostgres } from "./config.js";
/** module.exports = {
* Takes an expression such as 30d and returns a moment object of that date in future
* /**
* Key Shorthand * Takes an expression such as 30d and returns a moment object of that date in future
* ================== *
* years y * Key Shorthand
* quarters Q * ==================
* months M * years y
* weeks w * quarters Q
* days d * months M
* hours h * weeks w
* minutes m * days d
* seconds s * hours h
* milliseconds ms * minutes m
* * seconds s
* @param {String} expression * milliseconds ms
* @returns {Object} *
*/ * @param {String} expression
const parseDatePeriod = (expression) => { * @returns {Object}
const matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m); */
if (matches) { parseDatePeriod: function (expression) {
return moment().add(matches[1], matches[2]); let matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
if (matches) {
return moment().add(matches[1], matches[2]);
}
return null;
} }
return null;
}; };
const convertIntFieldsToBool = (obj, fields) => {
fields.forEach((field) => {
if (typeof obj[field] !== "undefined") {
obj[field] = obj[field] === 1;
}
});
return obj;
};
const convertBoolFieldsToInt = (obj, fields) => {
fields.forEach((field) => {
if (typeof obj[field] !== "undefined") {
obj[field] = obj[field] ? 1 : 0;
}
});
return obj;
};
/**
* Casts a column to json if using postgres
*
* @param {string} colName
* @returns {string|Objection.ReferenceBuilder}
*/
const castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName);
export { parseDatePeriod, convertIntFieldsToBool, convertBoolFieldsToInt, castJsonIfNeed };

View File

@@ -1,34 +1,33 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'identifier_for_migrate';
const logger = require('../logger').migrate;
const migrateName = "identifier_for_migrate";
/** /**
* Migrate * Migrate
* *
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (_knex) => { exports.up = function (knex, Promise) {
logger.info(`[${migrateName}] Migrating Up...`);
logger.info('[' + migrate_name + '] Migrating Up...');
// Create Table example: // Create Table example:
/* /*return knex.schema.createTable('notification', (table) => {
return knex.schema.createTable('notification', (table) => {
table.increments().primary(); table.increments().primary();
table.string('name').notNull(); table.string('name').notNull();
table.string('type').notNull(); table.string('type').notNull();
table.integer('created_on').notNull(); table.integer('created_on').notNull();
table.integer('modified_on').notNull(); table.integer('modified_on').notNull();
}) })
.then(function () { .then(function () {
logger.info('[' + migrateName + '] Notification Table created'); logger.info('[' + migrate_name + '] Notification Table created');
}); });*/
*/
logger.info(`[${migrateName}] Migrating Up Complete`); logger.info('[' + migrate_name + '] Migrating Up Complete');
return Promise.resolve(true); return Promise.resolve(true);
}; };
@@ -36,24 +35,21 @@ const up = (_knex) => {
/** /**
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.info(`[${migrateName}] Migrating Down...`); logger.info('[' + migrate_name + '] Migrating Down...');
// Drop table example: // Drop table example:
/* /*return knex.schema.dropTable('notification')
return knex.schema.dropTable('notification') .then(() => {
.then(() => { logger.info('[' + migrate_name + '] Notification Table dropped');
logger.info(`[${migrateName}] Notification Table dropped`); });*/
});
*/
logger.info(`[${migrateName}] Migrating Down Complete`); logger.info('[' + migrate_name + '] Migrating Down Complete');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,110 +1,106 @@
import { exec as nodeExec, execFile as nodeExecFile } from "node:child_process"; const _ = require('lodash');
import { dirname } from "node:path"; const exec = require('child_process').exec;
import { fileURLToPath } from "node:url"; const execFile = require('child_process').execFile;
import { Liquid } from "liquidjs"; const { Liquid } = require('liquidjs');
import _ from "lodash"; const logger = require('../logger').global;
import { global as logger } from "../logger.js"; const error = require('./error');
import errs from "./error.js";
const __filename = fileURLToPath(import.meta.url); module.exports = {
const __dirname = dirname(__filename);
const exec = async (cmd, options = {}) => { exec: async function(cmd, options = {}) {
logger.debug("CMD:", cmd); logger.debug('CMD:', cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => {
const child = nodeExec(cmd, options, (isError, stdout, stderr) => { const { stdout, stderr } = await new Promise((resolve, reject) => {
if (isError) { const child = exec(cmd, options, (isError, stdout, stderr) => {
reject(new errs.CommandError(stderr, isError)); if (isError) {
} else { reject(new error.CommandError(stderr, isError));
resolve({ stdout, stderr }); } else {
} resolve({ stdout, stderr });
}
});
child.on('error', (e) => {
reject(new error.CommandError(stderr, 1, e));
});
}); });
return stdout;
},
child.on("error", (e) => {
reject(new errs.CommandError(stderr, 1, e));
});
});
return stdout;
};
/**
* @param {String} cmd
* @param {Array} args
* @param {Object|undefined} options
* @returns {Promise}
*/
const execFile = (cmd, args, options) => {
logger.debug(`CMD: ${cmd} ${args ? args.join(" ") : ""}`);
const opts = options || {};
return new Promise((resolve, reject) => {
nodeExecFile(cmd, args, opts, (err, stdout, stderr) => {
if (err && typeof err === "object") {
reject(new errs.CommandError(stderr, 1, err));
} else {
resolve(stdout.trim());
}
});
});
};
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
const omitRow = (omissions) => {
/** /**
* @param {Object} row * @param {String} cmd
* @returns {Object} * @param {Array} args
* @returns {Promise}
*/ */
return (row) => { execFile: function (cmd, args) {
return _.omit(row, omissions); // logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
};
};
/** return new Promise((resolve, reject) => {
* Used in objection query builder execFile(cmd, args, function (err, stdout, /*stderr*/) {
* if (err && typeof err === 'object') {
* @param {Array} omissions reject(err);
* @returns {Function} } else {
*/ resolve(stdout.trim());
const omitRows = (omissions) => { }
/** });
* @param {Array} rows
* @returns {Object}
*/
return (rows) => {
rows.forEach((row, idx) => {
rows[idx] = _.omit(row, omissions);
}); });
return rows; },
};
};
/**
* @returns {Object} Liquid render engine
*/
const getRenderEngine = () => {
const renderEngine = new Liquid({
root: `${__dirname}/../templates/`,
});
/** /**
* nginxAccessRule expects the object given to have 2 properties: * Used in objection query builder
* *
* directive string * @param {Array} omissions
* address string * @returns {Function}
*/ */
renderEngine.registerFilter("nginxAccessRule", (v) => { omitRow: function (omissions) {
if (typeof v.directive !== "undefined" && typeof v.address !== "undefined" && v.directive && v.address) { /**
return `${v.directive} ${v.address};`; * @param {Object} row
} * @returns {Object}
return ""; */
}); return (row) => {
return _.omit(row, omissions);
};
},
return renderEngine; /**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
omitRows: function (omissions) {
/**
* @param {Array} rows
* @returns {Object}
*/
return (rows) => {
rows.forEach((row, idx) => {
rows[idx] = _.omit(row, omissions);
});
return rows;
};
},
/**
* @returns {Object} Liquid render engine
*/
getRenderEngine: function () {
const renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
/**
* nginxAccessRule expects the object given to have 2 properties:
*
* directive string
* address string
*/
renderEngine.registerFilter('nginxAccessRule', (v) => {
if (typeof v.directive !== 'undefined' && typeof v.address !== 'undefined' && v.directive && v.address) {
return `${v.directive} ${v.address};`;
}
return '';
});
return renderEngine;
}
}; };
export default { exec, execFile, omitRow, omitRows, getRenderEngine };

View File

@@ -1,12 +1,13 @@
import Ajv from "ajv/dist/2020.js"; const error = require('../error');
import errs from "../error.js"; const path = require('path');
const parser = require('json-schema-ref-parser');
const ajv = new Ajv({ const ajv = require('ajv')({
verbose: true, verbose: true,
allErrors: true, validateSchema: true,
allowUnionTypes: true, allErrors: false,
strict: false, format: 'full',
coerceTypes: true, coerceTypes: true
}); });
/** /**
@@ -14,27 +15,31 @@ 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 (typeof payload === 'undefined') {
} reject(new error.ValidationError('Payload is undefined'));
}
// Can't use falsy check here as valid payload could be `0` or `false` let validate = ajv.compile(schema);
if (typeof payload === "undefined") { let valid = validate(payload);
throw new errs.ValidationError("Payload is undefined");
}
const validate = ajv.compile(schema); if (valid && !validate.errors) {
const valid = validate(payload); resolve(payload);
} else {
let message = ajv.errorsText(validate.errors);
let err = new error.ValidationError(message);
err.debug = [validate.errors, payload];
reject(err);
}
});
}
if (valid && !validate.errors) { apiValidator.loadSchemas = parser
return payload; .dereference(path.resolve('schema/index.json'))
} .then((schema) => {
ajv.addSchema(schema);
return schema;
});
const message = ajv.errorsText(validate.errors); module.exports = apiValidator;
const err = new errs.ValidationError(message);
err.debug = [validate.errors, payload];
throw err;
};
export default apiValidator;

View File

@@ -1,17 +1,17 @@
import Ajv from 'ajv/dist/2020.js'; const _ = require('lodash');
import _ from "lodash"; const error = require('../error');
import commonDefinitions from "../../schema/common.json" with { type: "json" }; const definitions = require('../../schema/definitions.json');
import errs from "../error.js";
RegExp.prototype.toJSON = RegExp.prototype.toString; RegExp.prototype.toJSON = RegExp.prototype.toString;
const ajv = new Ajv({ const ajv = require('ajv')({
verbose: true, verbose: true,
allErrors: true, allErrors: true,
allowUnionTypes: true, format: 'full', // strict regexes for format checks
coerceTypes: true, coerceTypes: true,
strict: false, schemas: [
schemas: [commonDefinitions], definitions
]
}); });
/** /**
@@ -20,26 +20,30 @@ const ajv = new Ajv({
* @param {Object} payload * @param {Object} payload
* @returns {Promise} * @returns {Promise}
*/ */
const validator = (schema, payload) => { function validator (schema, payload) {
return new Promise((resolve, reject) => { return new Promise(function (resolve, reject) {
if (!payload) { if (!payload) {
reject(new errs.InternalValidationError("Payload is falsy")); reject(new error.InternalValidationError('Payload is falsy'));
} else { } else {
try { try {
const validate = ajv.compile(schema); let validate = ajv.compile(schema);
const valid = validate(payload);
let valid = validate(payload);
if (valid && !validate.errors) { if (valid && !validate.errors) {
resolve(_.cloneDeep(payload)); resolve(_.cloneDeep(payload));
} else { } else {
const message = ajv.errorsText(validate.errors); let message = ajv.errorsText(validate.errors);
reject(new errs.InternalValidationError(message)); reject(new error.InternalValidationError(message));
} }
} catch (err) { } catch (err) {
reject(err); reject(err);
} }
}
});
};
export default validator; }
});
}
module.exports = validator;

View File

@@ -1,18 +1,14 @@
import signale from "signale"; const {Signale} = require('signale');
const opts = { module.exports = {
logLevel: "info", global: new Signale({scope: 'Global '}),
migrate: new Signale({scope: 'Migrate '}),
express: new Signale({scope: 'Express '}),
access: new Signale({scope: 'Access '}),
nginx: new Signale({scope: 'Nginx '}),
ssl: new Signale({scope: 'SSL '}),
certbot: new Signale({scope: 'Certbot '}),
import: new Signale({scope: 'Importer '}),
setup: new Signale({scope: 'Setup '}),
ip_ranges: new Signale({scope: 'IP Ranges'})
}; };
const global = new signale.Signale({ scope: "Global ", ...opts });
const migrate = new signale.Signale({ scope: "Migrate ", ...opts });
const express = new signale.Signale({ scope: "Express ", ...opts });
const access = new signale.Signale({ scope: "Access ", ...opts });
const nginx = new signale.Signale({ scope: "Nginx ", ...opts });
const ssl = new signale.Signale({ scope: "SSL ", ...opts });
const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
const importer = new signale.Signale({ scope: "Importer ", ...opts });
const setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
export { global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges };

View File

@@ -1,13 +1,15 @@
import db from "./db.js"; const db = require('./db');
import { migrate as logger } from "./logger.js"; const logger = require('./logger').migrate;
const migrateUp = async () => { module.exports = {
const version = await db.migrate.currentVersion(); latest: function () {
logger.info("Current database version:", version); return db.migrate.currentVersion()
return await db.migrate.latest({ .then((version) => {
tableName: "migrations", logger.info('Current database version:', version);
directory: "migrations", return db.migrate.latest({
}); tableName: 'migrations',
directory: 'migrations'
});
});
}
}; };
export { migrateUp };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'initial-schema';
const logger = require('../logger').migrate;
const migrateName = "initial-schema";
/** /**
* Migrate * Migrate
@@ -8,199 +7,199 @@ const migrateName = "initial-schema";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.createTable('auth', (table) => {
.createTable("auth", (table) => { table.increments().primary();
table.increments().primary(); table.dateTime('created_on').notNull();
table.dateTime("created_on").notNull(); table.dateTime('modified_on').notNull();
table.dateTime("modified_on").notNull(); table.integer('user_id').notNull().unsigned();
table.integer("user_id").notNull().unsigned(); table.string('type', 30).notNull();
table.string("type", 30).notNull(); table.string('secret').notNull();
table.string("secret").notNull(); table.json('meta').notNull();
table.json("meta").notNull(); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.integer("is_deleted").notNull().unsigned().defaultTo(0); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] auth Table created`); logger.info('[' + migrate_name + '] auth Table created');
return knex.schema.createTable("user", (table) => { return knex.schema.createTable('user', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("is_deleted").notNull().unsigned().defaultTo(0); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.integer("is_disabled").notNull().unsigned().defaultTo(0); table.integer('is_disabled').notNull().unsigned().defaultTo(0);
table.string("email").notNull(); table.string('email').notNull();
table.string("name").notNull(); table.string('name').notNull();
table.string("nickname").notNull(); table.string('nickname').notNull();
table.string("avatar").notNull(); table.string('avatar').notNull();
table.json("roles").notNull(); table.json('roles').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] user Table created`); logger.info('[' + migrate_name + '] user Table created');
return knex.schema.createTable("user_permission", (table) => { return knex.schema.createTable('user_permission', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("user_id").notNull().unsigned(); table.integer('user_id').notNull().unsigned();
table.string("visibility").notNull(); table.string('visibility').notNull();
table.string("proxy_hosts").notNull(); table.string('proxy_hosts').notNull();
table.string("redirection_hosts").notNull(); table.string('redirection_hosts').notNull();
table.string("dead_hosts").notNull(); table.string('dead_hosts').notNull();
table.string("streams").notNull(); table.string('streams').notNull();
table.string("access_lists").notNull(); table.string('access_lists').notNull();
table.string("certificates").notNull(); table.string('certificates').notNull();
table.unique("user_id"); table.unique('user_id');
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] user_permission Table created`); logger.info('[' + migrate_name + '] user_permission Table created');
return knex.schema.createTable("proxy_host", (table) => { return knex.schema.createTable('proxy_host', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("owner_user_id").notNull().unsigned(); table.integer('owner_user_id').notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull(); table.json('domain_names').notNull();
table.string("forward_ip").notNull(); table.string('forward_ip').notNull();
table.integer("forward_port").notNull().unsigned(); table.integer('forward_port').notNull().unsigned();
table.integer("access_list_id").notNull().unsigned().defaultTo(0); table.integer('access_list_id').notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0); table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0); table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.integer("caching_enabled").notNull().unsigned().defaultTo(0); table.integer('caching_enabled').notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0); table.integer('block_exploits').notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo(""); table.text('advanced_config').notNull().defaultTo('');
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table created`); logger.info('[' + migrate_name + '] proxy_host Table created');
return knex.schema.createTable("redirection_host", (table) => { return knex.schema.createTable('redirection_host', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("owner_user_id").notNull().unsigned(); table.integer('owner_user_id').notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull(); table.json('domain_names').notNull();
table.string("forward_domain_name").notNull(); table.string('forward_domain_name').notNull();
table.integer("preserve_path").notNull().unsigned().defaultTo(0); table.integer('preserve_path').notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0); table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0); table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0); table.integer('block_exploits').notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo(""); table.text('advanced_config').notNull().defaultTo('');
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] redirection_host Table created`); logger.info('[' + migrate_name + '] redirection_host Table created');
return knex.schema.createTable("dead_host", (table) => { return knex.schema.createTable('dead_host', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("owner_user_id").notNull().unsigned(); table.integer('owner_user_id').notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull(); table.json('domain_names').notNull();
table.integer("certificate_id").notNull().unsigned().defaultTo(0); table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0); table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo(""); table.text('advanced_config').notNull().defaultTo('');
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] dead_host Table created`); logger.info('[' + migrate_name + '] dead_host Table created');
return knex.schema.createTable("stream", (table) => { return knex.schema.createTable('stream', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("owner_user_id").notNull().unsigned(); table.integer('owner_user_id').notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.integer("incoming_port").notNull().unsigned(); table.integer('incoming_port').notNull().unsigned();
table.string("forward_ip").notNull(); table.string('forward_ip').notNull();
table.integer("forwarding_port").notNull().unsigned(); table.integer('forwarding_port').notNull().unsigned();
table.integer("tcp_forwarding").notNull().unsigned().defaultTo(0); table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0);
table.integer("udp_forwarding").notNull().unsigned().defaultTo(0); table.integer('udp_forwarding').notNull().unsigned().defaultTo(0);
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] stream Table created`); logger.info('[' + migrate_name + '] stream Table created');
return knex.schema.createTable("access_list", (table) => { return knex.schema.createTable('access_list', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("owner_user_id").notNull().unsigned(); table.integer('owner_user_id').notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.string("name").notNull(); table.string('name').notNull();
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] access_list Table created`); logger.info('[' + migrate_name + '] access_list Table created');
return knex.schema.createTable("certificate", (table) => { return knex.schema.createTable('certificate', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("owner_user_id").notNull().unsigned(); table.integer('owner_user_id').notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0); table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.string("provider").notNull(); table.string('provider').notNull();
table.string("nice_name").notNull().defaultTo(""); table.string('nice_name').notNull().defaultTo('');
table.json("domain_names").notNull(); table.json('domain_names').notNull();
table.dateTime("expires_on").notNull(); table.dateTime('expires_on').notNull();
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] certificate Table created`); logger.info('[' + migrate_name + '] certificate Table created');
return knex.schema.createTable("access_list_auth", (table) => { return knex.schema.createTable('access_list_auth', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("access_list_id").notNull().unsigned(); table.integer('access_list_id').notNull().unsigned();
table.string("username").notNull(); table.string('username').notNull();
table.string("password").notNull(); table.string('password').notNull();
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] access_list_auth Table created`); logger.info('[' + migrate_name + '] access_list_auth Table created');
return knex.schema.createTable("audit_log", (table) => { return knex.schema.createTable('audit_log', (table) => {
table.increments().primary(); table.increments().primary();
table.dateTime("created_on").notNull(); table.dateTime('created_on').notNull();
table.dateTime("modified_on").notNull(); table.dateTime('modified_on').notNull();
table.integer("user_id").notNull().unsigned(); table.integer('user_id').notNull().unsigned();
table.string("object_type").notNull().defaultTo(""); table.string('object_type').notNull().defaultTo('');
table.integer("object_id").notNull().unsigned().defaultTo(0); table.integer('object_id').notNull().unsigned().defaultTo(0);
table.string("action").notNull(); table.string('action').notNull();
table.json("meta").notNull(); table.json('meta').notNull();
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] audit_log Table created`); logger.info('[' + migrate_name + '] audit_log Table created');
}); });
}; };
/** /**
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`); logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'websockets';
const logger = require('../logger').migrate;
const migrateName = "websockets";
/** /**
* Migrate * Migrate
@@ -8,29 +7,29 @@ const migrateName = "websockets";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('proxy_host', function (proxy_host) {
.table("proxy_host", (proxy_host) => { proxy_host.integer('allow_websocket_upgrade').notNull().unsigned().defaultTo(0);
proxy_host.integer("allow_websocket_upgrade").notNull().unsigned().defaultTo(0); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info('[' + migrate_name + '] proxy_host Table altered');
}); });
}; };
/** /**
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'forward_host';
const logger = require('../logger').migrate;
const migrateName = "forward_host";
/** /**
* Migrate * Migrate
@@ -8,17 +7,17 @@ const migrateName = "forward_host";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('proxy_host', function (proxy_host) {
.table("proxy_host", (proxy_host) => { proxy_host.renameColumn('forward_ip', 'forward_host');
proxy_host.renameColumn("forward_ip", "forward_host"); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info('[' + migrate_name + '] proxy_host Table altered');
}); });
}; };
@@ -26,11 +25,10 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'http2_support';
const logger = require('../logger').migrate;
const migrateName = "http2_support";
/** /**
* Migrate * Migrate
@@ -8,31 +7,31 @@ const migrateName = "http2_support";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('proxy_host', function (proxy_host) {
.table("proxy_host", (proxy_host) => { proxy_host.integer('http2_support').notNull().unsigned().defaultTo(0);
proxy_host.integer("http2_support").notNull().unsigned().defaultTo(0); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table("redirection_host", (redirection_host) => { return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer("http2_support").notNull().unsigned().defaultTo(0); redirection_host.integer('http2_support').notNull().unsigned().defaultTo(0);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`); logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema.table("dead_host", (dead_host) => { return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer("http2_support").notNull().unsigned().defaultTo(0); dead_host.integer('http2_support').notNull().unsigned().defaultTo(0);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] dead_host Table altered`); logger.info('[' + migrate_name + '] dead_host Table altered');
}); });
}; };
@@ -40,11 +39,11 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'forward_scheme';
const logger = require('../logger').migrate;
const migrateName = "forward_scheme";
/** /**
* Migrate * Migrate
@@ -8,17 +7,17 @@ const migrateName = "forward_scheme";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('proxy_host', function (proxy_host) {
.table("proxy_host", (proxy_host) => { proxy_host.string('forward_scheme').notNull().defaultTo('http');
proxy_host.string("forward_scheme").notNull().defaultTo("http"); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info('[' + migrate_name + '] proxy_host Table altered');
}); });
}; };
@@ -26,11 +25,10 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'disabled';
const logger = require('../logger').migrate;
const migrateName = "disabled";
/** /**
* Migrate * Migrate
@@ -8,38 +7,38 @@ const migrateName = "disabled";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('proxy_host', function (proxy_host) {
.table("proxy_host", (proxy_host) => { proxy_host.integer('enabled').notNull().unsigned().defaultTo(1);
proxy_host.integer("enabled").notNull().unsigned().defaultTo(1); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table("redirection_host", (redirection_host) => { return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer("enabled").notNull().unsigned().defaultTo(1); redirection_host.integer('enabled').notNull().unsigned().defaultTo(1);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`); logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema.table("dead_host", (dead_host) => { return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer("enabled").notNull().unsigned().defaultTo(1); dead_host.integer('enabled').notNull().unsigned().defaultTo(1);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] dead_host Table altered`); logger.info('[' + migrate_name + '] dead_host Table altered');
return knex.schema.table("stream", (stream) => { return knex.schema.table('stream', function (stream) {
stream.integer("enabled").notNull().unsigned().defaultTo(1); stream.integer('enabled').notNull().unsigned().defaultTo(1);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] stream Table altered`); logger.info('[' + migrate_name + '] stream Table altered');
}); });
}; };
@@ -47,11 +46,10 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'custom_locations';
const logger = require('../logger').migrate;
const migrateName = "custom_locations";
/** /**
* Migrate * Migrate
@@ -9,17 +8,17 @@ const migrateName = "custom_locations";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('proxy_host', function (proxy_host) {
.table("proxy_host", (proxy_host) => { proxy_host.json('locations');
proxy_host.json("locations"); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info('[' + migrate_name + '] proxy_host Table altered');
}); });
}; };
@@ -27,11 +26,10 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'hsts';
const logger = require('../logger').migrate;
const migrateName = "hsts";
/** /**
* Migrate * Migrate
@@ -8,34 +7,34 @@ const migrateName = "hsts";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('proxy_host', function (proxy_host) {
.table("proxy_host", (proxy_host) => { proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
proxy_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0); proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
proxy_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`); logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table("redirection_host", (redirection_host) => { return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0); redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
redirection_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0); redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`); logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema.table("dead_host", (dead_host) => { return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0); dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
dead_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0); dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] dead_host Table altered`); logger.info('[' + migrate_name + '] dead_host Table altered');
}); });
}; };
@@ -43,11 +42,10 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'settings';
const logger = require('../logger').migrate;
const migrateName = "settings";
/** /**
* Migrate * Migrate
@@ -8,10 +7,11 @@ const migrateName = "settings";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.createTable('setting', (table) => { return knex.schema.createTable('setting', (table) => {
table.string('id').notNull().primary(); table.string('id').notNull().primary();
@@ -21,7 +21,7 @@ const up = (knex) => {
table.json('meta').notNull(); table.json('meta').notNull();
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] setting Table created`); logger.info('[' + migrate_name + '] setting Table created');
}); });
}; };
@@ -29,11 +29,10 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`); logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'access_list_client';
const logger = require('../logger').migrate;
const migrateName = "access_list_client";
/** /**
* Migrate * Migrate
@@ -8,30 +7,32 @@ const migrateName = "access_list_client";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema logger.info('[' + migrate_name + '] Migrating Up...');
.createTable("access_list_client", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("access_list_id").notNull().unsigned();
table.string("address").notNull();
table.string("directive").notNull();
table.json("meta").notNull();
})
.then(() => {
logger.info(`[${migrateName}] access_list_client Table created`);
return knex.schema.table("access_list", (access_list) => { return knex.schema.createTable('access_list_client', (table) => {
access_list.integer("satify_any").notNull().defaultTo(0); table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('access_list_id').notNull().unsigned();
table.string('address').notNull();
table.string('directive').notNull();
table.json('meta').notNull();
})
.then(function () {
logger.info('[' + migrate_name + '] access_list_client Table created');
return knex.schema.table('access_list', function (access_list) {
access_list.integer('satify_any').notNull().defaultTo(0);
}); });
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] access_list Table altered`); logger.info('[' + migrate_name + '] access_list Table altered');
}); });
}; };
@@ -39,14 +40,14 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (knex) => { exports.down = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Down...`); logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema.dropTable("access_list_client").then(() => { return knex.schema.dropTable('access_list_client')
logger.info(`[${migrateName}] access_list_client Table dropped`); .then(() => {
}); logger.info('[' + migrate_name + '] access_list_client Table dropped');
});
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'access_list_client_fix';
const logger = require('../logger').migrate;
const migrateName = "access_list_client_fix";
/** /**
* Migrate * Migrate
@@ -8,17 +7,17 @@ const migrateName = "access_list_client_fix";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`); logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('access_list', function (access_list) {
.table("access_list", (access_list) => { access_list.renameColumn('satify_any', 'satisfy_any');
access_list.renameColumn("satify_any", "satisfy_any"); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] access_list Table altered`); logger.info('[' + migrate_name + '] access_list Table altered');
}); });
}; };
@@ -26,11 +25,10 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (_knex) => { exports.down = function (knex, Promise) {
logger.warn(`[${migrateName}] You can't migrate down this one.`); logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true); return Promise.resolve(true);
}; };
export { up, down };

View File

@@ -0,0 +1,48 @@
const migrate_name = 'openid_connect';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('openidc_enabled').notNull().unsigned().defaultTo(0);
proxy_host.text('openidc_redirect_uri').notNull().defaultTo('');
proxy_host.text('openidc_discovery').notNull().defaultTo('');
proxy_host.text('openidc_auth_method').notNull().defaultTo('');
proxy_host.text('openidc_client_id').notNull().defaultTo('');
proxy_host.text('openidc_client_secret').notNull().defaultTo('');
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.dropColumn('openidc_enabled');
proxy_host.dropColumn('openidc_redirect_uri');
proxy_host.dropColumn('openidc_discovery');
proxy_host.dropColumn('openidc_auth_method');
proxy_host.dropColumn('openidc_client_id');
proxy_host.dropColumn('openidc_client_secret');
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
});
};

View File

@@ -0,0 +1,40 @@
const migrate_name = 'openid_allowed_users';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('openidc_restrict_users_enabled').notNull().unsigned().defaultTo(0);
proxy_host.json('openidc_allowed_users').notNull().defaultTo([]);
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.dropColumn('openidc_restrict_users_enabled');
proxy_host.dropColumn('openidc_allowed_users');
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
});
};

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'pass_auth';
const logger = require('../logger').migrate;
const migrateName = "pass_auth";
/** /**
* Migrate * Migrate
@@ -8,17 +7,18 @@ const migrateName = "pass_auth";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema logger.info('[' + migrate_name + '] Migrating Up...');
.table("access_list", (access_list) => {
access_list.integer("pass_auth").notNull().defaultTo(1); return knex.schema.table('access_list', function (access_list) {
}) access_list.integer('pass_auth').notNull().defaultTo(1);
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] access_list Table altered`); logger.info('[' + migrate_name + '] access_list Table altered');
}); });
}; };
@@ -26,18 +26,16 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (knex) => { exports.down = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Down...`); logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema return knex.schema.table('access_list', function (access_list) {
.table("access_list", (access_list) => { access_list.dropColumn('pass_auth');
access_list.dropColumn("pass_auth"); })
})
.then(() => { .then(() => {
logger.info(`[${migrateName}] access_list pass_auth Column dropped`); logger.info('[' + migrate_name + '] access_list pass_auth Column dropped');
}); });
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'redirection_scheme';
const logger = require('../logger').migrate;
const migrateName = "redirection_scheme";
/** /**
* Migrate * Migrate
@@ -8,17 +7,18 @@ const migrateName = "redirection_scheme";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema logger.info('[' + migrate_name + '] Migrating Up...');
.table("redirection_host", (table) => {
table.string("forward_scheme").notNull().defaultTo("$scheme"); return knex.schema.table('redirection_host', (table) => {
}) table.string('forward_scheme').notNull().defaultTo('$scheme');
.then(() => { })
logger.info(`[${migrateName}] redirection_host Table altered`); .then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
}); });
}; };
@@ -26,18 +26,16 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (knex) => { exports.down = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Down...`); logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema return knex.schema.table('redirection_host', (table) => {
.table("redirection_host", (table) => { table.dropColumn('forward_scheme');
table.dropColumn("forward_scheme"); })
}) .then(function () {
.then(() => { logger.info('[' + migrate_name + '] redirection_host Table altered');
logger.info(`[${migrateName}] redirection_host Table altered`);
}); });
}; };
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'redirection_status_code';
const logger = require('../logger').migrate;
const migrateName = "redirection_status_code";
/** /**
* Migrate * Migrate
@@ -8,17 +7,18 @@ const migrateName = "redirection_status_code";
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const up = (knex) => { exports.up = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema logger.info('[' + migrate_name + '] Migrating Up...');
.table("redirection_host", (table) => {
table.integer("forward_http_code").notNull().unsigned().defaultTo(302); return knex.schema.table('redirection_host', (table) => {
}) table.integer('forward_http_code').notNull().unsigned().defaultTo(302);
.then(() => { })
logger.info(`[${migrateName}] redirection_host Table altered`); .then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
}); });
}; };
@@ -26,18 +26,16 @@ const up = (knex) => {
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @param {Promise} Promise
* @returns {Promise} * @returns {Promise}
*/ */
const down = (knex) => { exports.down = function (knex/*, Promise*/) {
logger.info(`[${migrateName}] Migrating Down...`); logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema return knex.schema.table('redirection_host', (table) => {
.table("redirection_host", (table) => { table.dropColumn('forward_http_code');
table.dropColumn("forward_http_code"); })
}) .then(function () {
.then(() => { logger.info('[' + migrate_name + '] redirection_host Table altered');
logger.info(`[${migrateName}] redirection_host Table altered`);
}); });
}; };
export { up, down };

View File

@@ -1,43 +1,40 @@
import { migrate as logger } from "../logger.js"; const migrate_name = 'stream_domain';
const logger = require('../logger').migrate;
const migrateName = "stream_domain";
/** /**
* Migrate * Migrate
* *
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @returns {Promise} * @param {Promise} Promise
*/ * @returns {Promise}
const up = (knex) => { */
logger.info(`[${migrateName}] Migrating Up...`); exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema return knex.schema.table('stream', (table) => {
.table("stream", (table) => { table.renameColumn('forward_ip', 'forwarding_host');
table.renameColumn("forward_ip", "forwarding_host"); })
}) .then(function () {
.then(() => { logger.info('[' + migrate_name + '] stream Table altered');
logger.info(`[${migrateName}] stream Table altered`);
}); });
}; };
/** /**
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @returns {Promise} * @param {Promise} Promise
*/ * @returns {Promise}
const down = (knex) => { */
logger.info(`[${migrateName}] Migrating Down...`); exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema return knex.schema.table('stream', (table) => {
.table("stream", (table) => { table.renameColumn('forwarding_host', 'forward_ip');
table.renameColumn("forwarding_host", "forward_ip"); })
}) .then(function () {
.then(() => { logger.info('[' + migrate_name + '] stream Table altered');
logger.info(`[${migrateName}] stream Table altered`);
}); });
}; };
export { up, down };

View File

@@ -1,19 +1,17 @@
import internalNginx from "../internal/nginx.js"; const migrate_name = 'stream_domain';
import { migrate as logger } from "../logger.js"; const logger = require('../logger').migrate;
const internalNginx = require('../internal/nginx');
const migrateName = "stream_domain";
async function regenerateDefaultHost(knex) { async function regenerateDefaultHost(knex) {
const row = await knex("setting").select("*").where("id", "default-site").first(); const row = await knex('setting').select('*').where('id', 'default-site').first();
if (!row) { if (!row) {
return Promise.resolve(); return Promise.resolve();
} }
return internalNginx return internalNginx.deleteConfig('default')
.deleteConfig("default")
.then(() => { .then(() => {
return internalNginx.generateConfig("default", row); return internalNginx.generateConfig('default', row);
}) })
.then(() => { .then(() => {
return internalNginx.test(); return internalNginx.test();
@@ -24,29 +22,29 @@ async function regenerateDefaultHost(knex) {
} }
/** /**
* Migrate * Migrate
* *
* @see http://knexjs.org/#Schema * @see http://knexjs.org/#Schema
* *
* @param {Object} knex * @param {Object} knex
* @returns {Promise} * @param {Promise} Promise
*/ * @returns {Promise}
const up = (knex) => { */
logger.info(`[${migrateName}] Migrating Up...`); exports.up = function (knex) {
logger.info('[' + migrate_name + '] Migrating Up...');
return regenerateDefaultHost(knex); return regenerateDefaultHost(knex);
}; };
/** /**
* Undo Migrate * Undo Migrate
* *
* @param {Object} knex * @param {Object} knex
* @returns {Promise} * @param {Promise} Promise
*/ * @returns {Promise}
const down = (knex) => { */
logger.info(`[${migrateName}] Migrating Down...`); exports.down = function (knex) {
logger.info('[' + migrate_name + '] Migrating Down...');
return regenerateDefaultHost(knex); return regenerateDefaultHost(knex);
}; };
export { up, down };

View File

@@ -1,43 +0,0 @@
import { migrate as logger } from "../logger.js";
const migrateName = "stream_ssl";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("stream", (table) => {
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("stream", (table) => {
table.dropColumn("certificate_id");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
export { up, down };

View File

@@ -1,98 +1,86 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; const User = require('./user');
import AccessListAuth from "./access_list_auth.js"; const AccessListAuth = require('./access_list_auth');
import AccessListClient from "./access_list_client.js"; const AccessListClient = require('./access_list_client');
import now from "./now_helper.js"; const now = require('./now_helper');
import ProxyHostModel from "./proxy_host.js";
import User from "./user.js";
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
class AccessList extends Model { class AccessList extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'AccessList';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'access_list';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "AccessList"; return ['meta'];
} }
static get tableName() { static get relationMappings () {
return "access_list"; const ProxyHost = require('./proxy_host');
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return { return {
owner: { owner: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: User, modelClass: User,
join: { join: {
from: "access_list.owner_user_id", from: 'access_list.owner_user_id',
to: "user.id", to: 'user.id'
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
}, },
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
}, },
items: { items: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: AccessListAuth, modelClass: AccessListAuth,
join: { join: {
from: "access_list.id", from: 'access_list.id',
to: "access_list_auth.access_list_id", to: 'access_list_auth.access_list_id'
}, }
}, },
clients: { clients: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: AccessListClient, modelClass: AccessListClient,
join: { join: {
from: "access_list.id", from: 'access_list.id',
to: "access_list_client.access_list_id", to: 'access_list_client.access_list_id'
}, }
}, },
proxy_hosts: { proxy_hosts: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: ProxyHostModel, modelClass: ProxyHost,
join: { join: {
from: "access_list.id", from: 'access_list.id',
to: "proxy_host.access_list_id", to: 'proxy_host.access_list_id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("proxy_host.is_deleted", 0); qb.where('proxy_host.is_deleted', 0);
}, }
}, }
}; };
} }
} }
export default AccessList; module.exports = AccessList;

View File

@@ -1,55 +1,54 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import accessListModel from "./access_list.js"; const now = require('./now_helper');
import now from "./now_helper.js";
Model.knex(db); Model.knex(db);
class AccessListAuth extends Model { class AccessListAuth extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
} }
static get name() { static get name () {
return "AccessListAuth"; return 'AccessListAuth';
} }
static get tableName() { static get tableName () {
return "access_list_auth"; return 'access_list_auth';
} }
static get jsonAttributes() { static get jsonAttributes () {
return ["meta"]; return ['meta'];
} }
static get relationMappings() { static get relationMappings () {
return { return {
access_list: { access_list: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: accessListModel, modelClass: require('./access_list'),
join: { join: {
from: "access_list_auth.access_list_id", from: 'access_list_auth.access_list_id',
to: "access_list.id", to: 'access_list.id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("access_list.is_deleted", 0); qb.where('access_list.is_deleted', 0);
}, }
}, }
}; };
} }
} }
export default AccessListAuth; module.exports = AccessListAuth;

View File

@@ -1,55 +1,54 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import accessListModel from "./access_list.js"; const now = require('./now_helper');
import now from "./now_helper.js";
Model.knex(db); Model.knex(db);
class AccessListClient extends Model { class AccessListClient extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
} }
static get name() { static get name () {
return "AccessListClient"; return 'AccessListClient';
} }
static get tableName() { static get tableName () {
return "access_list_client"; return 'access_list_client';
} }
static get jsonAttributes() { static get jsonAttributes () {
return ["meta"]; return ['meta'];
} }
static get relationMappings() { static get relationMappings () {
return { return {
access_list: { access_list: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: accessListModel, modelClass: require('./access_list'),
join: { join: {
from: "access_list_client.access_list_id", from: 'access_list_client.access_list_id',
to: "access_list.id", to: 'access_list.id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("access_list.is_deleted", 0); qb.where('access_list.is_deleted', 0);
}, }
}, }
}; };
} }
} }
export default AccessListClient; module.exports = AccessListClient;

View File

@@ -1,52 +1,52 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import now from "./now_helper.js"; const User = require('./user');
import User from "./user.js"; const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class AuditLog extends Model { class AuditLog extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
} }
static get name() { static get name () {
return "AuditLog"; return 'AuditLog';
} }
static get tableName() { static get tableName () {
return "audit_log"; return 'audit_log';
} }
static get jsonAttributes() { static get jsonAttributes () {
return ["meta"]; return ['meta'];
} }
static get relationMappings() { static get relationMappings () {
return { return {
user: { user: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: User, modelClass: User,
join: { join: {
from: "audit_log.user_id", from: 'audit_log.user_id',
to: "user.id", to: 'user.id'
}, }
}, }
}; };
} }
} }
export default AuditLog; module.exports = AuditLog;

View File

@@ -1,92 +1,83 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import bcrypt from "bcrypt"; const bcrypt = require('bcrypt');
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; const User = require('./user');
import now from "./now_helper.js"; const now = require('./now_helper');
import User from "./user.js";
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted"]; function encryptPassword () {
/* jshint -W040 */
let _this = this;
function encryptPassword() { if (_this.type === 'password' && _this.secret) {
if (this.type === "password" && this.secret) { return bcrypt.hash(_this.secret, 13)
return bcrypt.hash(this.secret, 13).then((hash) => { .then(function (hash) {
this.secret = hash; _this.secret = hash;
}); });
} }
return null; return null;
} }
class Auth extends Model { class Auth extends Model {
$beforeInsert(queryContext) { $beforeInsert (queryContext) {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
return encryptPassword.apply(this, queryContext); return encryptPassword.apply(this, queryContext);
} }
$beforeUpdate(queryContext) { $beforeUpdate (queryContext) {
this.modified_on = now(); this.modified_on = now();
return encryptPassword.apply(this, queryContext); return encryptPassword.apply(this, queryContext);
} }
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
/** /**
* Verify a plain password against the encrypted password * Verify a plain password against the encrypted password
* *
* @param {String} password * @param {String} password
* @returns {Promise} * @returns {Promise}
*/ */
verifyPassword(password) { verifyPassword (password) {
return bcrypt.compare(password, this.secret); return bcrypt.compare(password, this.secret);
} }
static get name() { static get name () {
return "Auth"; return 'Auth';
} }
static get tableName() { static get tableName () {
return "auth"; return 'auth';
} }
static get jsonAttributes() { static get jsonAttributes () {
return ["meta"]; return ['meta'];
} }
static get relationMappings() { static get relationMappings () {
return { return {
user: { user: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: User, modelClass: User,
join: { join: {
from: "auth.user_id", from: 'auth.user_id',
to: "user.id", to: 'user.id'
}, },
filter: { filter: {
is_deleted: 0, is_deleted: 0
}, }
}, }
}; };
} }
} }
export default Auth; module.exports = Auth;

View File

@@ -1,121 +1,72 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; const User = require('./user');
import deadHostModel from "./dead_host.js"; const now = require('./now_helper');
import now from "./now_helper.js";
import proxyHostModel from "./proxy_host.js";
import redirectionHostModel from "./redirection_host.js";
import userModel from "./user.js";
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted"];
class Certificate extends Model { class Certificate extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for expires_on // Default for expires_on
if (typeof this.expires_on === "undefined") { if (typeof this.expires_on === 'undefined') {
this.expires_on = now(); this.expires_on = now();
} }
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === "undefined") { if (typeof this.domain_names === 'undefined') {
this.domain_names = []; this.domain_names = [];
} }
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
this.domain_names.sort(); this.domain_names.sort();
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== "undefined") { if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort(); this.domain_names.sort();
} }
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'Certificate';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'certificate';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "Certificate"; return ['domain_names', 'meta'];
} }
static get tableName() { static get relationMappings () {
return "certificate";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings() {
return { return {
owner: { owner: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: userModel, modelClass: User,
join: { join: {
from: "certificate.owner_user_id", from: 'certificate.owner_user_id',
to: "user.id", to: 'user.id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("user.is_deleted", 0); qb.where('user.is_deleted', 0);
}, }
}, }
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: proxyHostModel,
join: {
from: "certificate.id",
to: "proxy_host.certificate_id",
},
modify: (qb) => {
qb.where("proxy_host.is_deleted", 0);
},
},
dead_hosts: {
relation: Model.HasManyRelation,
modelClass: deadHostModel,
join: {
from: "certificate.id",
to: "dead_host.certificate_id",
},
modify: (qb) => {
qb.where("dead_host.is_deleted", 0);
},
},
redirection_hosts: {
relation: Model.HasManyRelation,
modelClass: redirectionHostModel,
join: {
from: "certificate.id",
to: "redirection_host.certificate_id",
},
modify: (qb) => {
qb.where("redirection_host.is_deleted", 0);
},
},
}; };
} }
} }
export default Certificate; module.exports = Certificate;

View File

@@ -1,92 +1,79 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; const User = require('./user');
import Certificate from "./certificate.js"; const Certificate = require('./certificate');
import now from "./now_helper.js"; const now = require('./now_helper');
import User from "./user.js";
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
class DeadHost extends Model { class DeadHost extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === "undefined") { if (typeof this.domain_names === 'undefined') {
this.domain_names = []; this.domain_names = [];
} }
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
this.domain_names.sort(); this.domain_names.sort();
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== "undefined") { if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort(); this.domain_names.sort();
} }
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'DeadHost';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'dead_host';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "DeadHost"; return ['domain_names', 'meta'];
} }
static get tableName() { static get relationMappings () {
return "dead_host";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings() {
return { return {
owner: { owner: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: User, modelClass: User,
join: { join: {
from: "dead_host.owner_user_id", from: 'dead_host.owner_user_id',
to: "user.id", to: 'user.id'
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
}, },
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
}, },
certificate: { certificate: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: Certificate, modelClass: Certificate,
join: { join: {
from: "dead_host.certificate_id", from: 'dead_host.certificate_id',
to: "certificate.id", to: 'certificate.id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("certificate.is_deleted", 0); qb.where('certificate.is_deleted', 0);
}, }
}, }
}; };
} }
} }
export default DeadHost; module.exports = DeadHost;

View File

@@ -1,12 +1,13 @@
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const config = require('../lib/config');
import { isSqlite } from "../lib/config.js"; const Model = require('objection').Model;
Model.knex(db); Model.knex(db);
export default () => { module.exports = function () {
if (isSqlite()) { if (config.isSqlite()) {
// eslint-disable-next-line
return Model.raw("datetime('now','localtime')"); return Model.raw("datetime('now','localtime')");
} }
return Model.raw("NOW()"); return Model.raw('NOW()');
}; };

View File

@@ -1,114 +1,107 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; const User = require('./user');
import AccessList from "./access_list.js"; const AccessList = require('./access_list');
import Certificate from "./certificate.js"; const Certificate = require('./certificate');
import now from "./now_helper.js"; const now = require('./now_helper');
import User from "./user.js";
Model.knex(db); Model.knex(db);
const boolFields = [
"is_deleted",
"ssl_forced",
"caching_enabled",
"block_exploits",
"allow_websocket_upgrade",
"http2_support",
"enabled",
"hsts_enabled",
"hsts_subdomains",
];
class ProxyHost extends Model { class ProxyHost extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === "undefined") { if (typeof this.domain_names === 'undefined') {
this.domain_names = []; this.domain_names = [];
} }
// Default for openidc_allowed_users
if (typeof this.openidc_allowed_users === 'undefined') {
this.openidc_allowed_users = [];
}
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
// Openidc defaults
if (typeof this.openidc_auth_method === 'undefined') {
this.openidc_auth_method = 'client_secret_post';
}
this.domain_names.sort(); this.domain_names.sort();
this.openidc_allowed_users.sort();
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== "undefined") { if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort(); this.domain_names.sort();
} }
// Sort openidc_allowed_users
if (typeof this.openidc_allowed_users !== 'undefined') {
this.openidc_allowed_users.sort();
}
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'ProxyHost';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'proxy_host';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "ProxyHost"; return ['domain_names', 'meta', 'locations', 'openidc_allowed_users'];
} }
static get tableName() { static get relationMappings () {
return "proxy_host";
}
static get jsonAttributes() {
return ["domain_names", "meta", "locations"];
}
static get relationMappings() {
return { return {
owner: { owner: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: User, modelClass: User,
join: { join: {
from: "proxy_host.owner_user_id", from: 'proxy_host.owner_user_id',
to: "user.id", to: 'user.id'
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
}, },
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
}, },
access_list: { access_list: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: AccessList, modelClass: AccessList,
join: { join: {
from: "proxy_host.access_list_id", from: 'proxy_host.access_list_id',
to: "access_list.id", to: 'access_list.id'
},
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
}, },
modify: function (qb) {
qb.where('access_list.is_deleted', 0);
}
}, },
certificate: { certificate: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: Certificate, modelClass: Certificate,
join: { join: {
from: "proxy_host.certificate_id", from: 'proxy_host.certificate_id',
to: "certificate.id", to: 'certificate.id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("certificate.is_deleted", 0); qb.where('certificate.is_deleted', 0);
}, }
}, }
}; };
} }
} }
export default ProxyHost; module.exports = ProxyHost;

View File

@@ -1,101 +1,80 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; const User = require('./user');
import Certificate from "./certificate.js"; const Certificate = require('./certificate');
import now from "./now_helper.js"; const now = require('./now_helper');
import User from "./user.js";
Model.knex(db); Model.knex(db);
const boolFields = [
"is_deleted",
"enabled",
"preserve_path",
"ssl_forced",
"block_exploits",
"hsts_enabled",
"hsts_subdomains",
"http2_support",
];
class RedirectionHost extends Model { class RedirectionHost extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for domain_names // Default for domain_names
if (typeof this.domain_names === "undefined") { if (typeof this.domain_names === 'undefined') {
this.domain_names = []; this.domain_names = [];
} }
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
this.domain_names.sort(); this.domain_names.sort();
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
// Sort domain_names // Sort domain_names
if (typeof this.domain_names !== "undefined") { if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort(); this.domain_names.sort();
} }
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'RedirectionHost';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'redirection_host';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "RedirectionHost"; return ['domain_names', 'meta'];
} }
static get tableName() { static get relationMappings () {
return "redirection_host";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings() {
return { return {
owner: { owner: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: User, modelClass: User,
join: { join: {
from: "redirection_host.owner_user_id", from: 'redirection_host.owner_user_id',
to: "user.id", to: 'user.id'
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
}, },
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
}, },
certificate: { certificate: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: Certificate, modelClass: Certificate,
join: { join: {
from: "redirection_host.certificate_id", from: 'redirection_host.certificate_id',
to: "certificate.id", to: 'certificate.id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("certificate.is_deleted", 0); qb.where('certificate.is_deleted', 0);
}, }
}, }
}; };
} }
} }
export default RedirectionHost; module.exports = RedirectionHost;

View File

@@ -1,8 +1,8 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
Model.knex(db); Model.knex(db);
@@ -27,4 +27,4 @@ class Setting extends Model {
} }
} }
export default Setting; module.exports = Setting;

View File

@@ -1,77 +1,55 @@
import { Model } from "objection"; // Objection Docs:
import db from "../db.js"; // http://vincit.github.io/objection.js/
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js"; const db = require('../db');
import now from "./now_helper.js"; const Model = require('objection').Model;
import User from "./user.js"; const User = require('./user');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
class Stream extends Model { class Stream extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === "undefined") { if (typeof this.meta === 'undefined') {
this.meta = {}; this.meta = {};
} }
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'Stream';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'stream';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "Stream"; return ['meta'];
} }
static get tableName() { static get relationMappings () {
return "stream";
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return { return {
owner: { owner: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: User, modelClass: User,
join: { join: {
from: "stream.owner_user_id", from: 'stream.owner_user_id',
to: "user.id", to: 'user.id'
}, },
modify: (qb) => { modify: function (qb) {
qb.where("user.is_deleted", 0); qb.where('user.is_deleted', 0);
}, }
}, }
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "stream.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
}; };
} }
} }
export default Stream; module.exports = Stream;

View File

@@ -3,17 +3,17 @@
and then has abilities after that. and then has abilities after that.
*/ */
import crypto from "node:crypto"; const _ = require('lodash');
import jwt from "jsonwebtoken"; const jwt = require('jsonwebtoken');
import _ from "lodash"; const crypto = require('crypto');
import { getPrivateKey, getPublicKey } from "../lib/config.js"; const config = require('../lib/config');
import errs from "../lib/error.js"; const error = require('../lib/error');
import { global as logger } from "../logger.js"; const logger = require('../logger').global;
const ALGO = 'RS256';
const ALGO = "RS256"; module.exports = function () {
export default () => { let token_data = {};
let tokenData = {};
const self = { const self = {
/** /**
@@ -21,26 +21,28 @@ export default () => {
* @returns {Promise} * @returns {Promise}
*/ */
create: (payload) => { create: (payload) => {
if (!getPrivateKey()) { if (!config.getPrivateKey()) {
logger.error("Private key is empty!"); logger.error('Private key is empty!');
} }
// sign with RSA SHA256 // sign with RSA SHA256
const options = { const options = {
algorithm: ALGO, algorithm: ALGO,
expiresIn: payload.expiresIn || "1d", expiresIn: payload.expiresIn || '1d'
}; };
payload.jti = crypto.randomBytes(12).toString("base64").substring(-8); payload.jti = crypto.randomBytes(12)
.toString('base64')
.substring(-8);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jwt.sign(payload, getPrivateKey(), options, (err, token) => { jwt.sign(payload, config.getPrivateKey(), options, (err, token) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
tokenData = payload; token_data = payload;
resolve({ resolve({
token: token, token: token,
payload: payload, payload: payload
}); });
} }
}); });
@@ -51,47 +53,42 @@ export default () => {
* @param {String} token * @param {String} token
* @returns {Promise} * @returns {Promise}
*/ */
load: (token) => { load: function (token) {
if (!getPublicKey()) { if (!config.getPublicKey()) {
logger.error("Public key is empty!"); logger.error('Public key is empty!');
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
if (!token || token === null || token === "null") { if (!token || token === null || token === 'null') {
reject(new errs.AuthError("Empty token")); reject(new error.AuthError('Empty token'));
} else { } else {
jwt.verify( jwt.verify(token, config.getPublicKey(), {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => {
token, if (err) {
getPublicKey(),
{ ignoreExpiration: false, algorithms: [ALGO] }, if (err.name === 'TokenExpiredError') {
(err, result) => { reject(new error.AuthError('Token has expired', err));
if (err) {
if (err.name === "TokenExpiredError") {
reject(new errs.AuthError("Token has expired", err));
} else {
reject(err);
}
} else { } else {
tokenData = result; reject(err);
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user.
if (
typeof tokenData.scope !== "undefined" &&
_.indexOf(tokenData.scope, "all") !== -1
) {
tokenData.scope = ["user"];
}
resolve(tokenData);
} }
},
); } else {
token_data = result;
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user.
if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) {
token_data.scope = ['user'];
}
resolve(token_data);
}
});
} }
} catch (err) { } catch (err) {
reject(err); reject(err);
} }
}); });
}, },
/** /**
@@ -100,15 +97,17 @@ export default () => {
* @param {String} scope * @param {String} scope
* @returns {Boolean} * @returns {Boolean}
*/ */
hasScope: (scope) => typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, scope) !== -1, hasScope: function (scope) {
return typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, scope) !== -1;
},
/** /**
* @param {String} key * @param {String} key
* @return {*} * @return {*}
*/ */
get: (key) => { get: function (key) {
if (typeof tokenData[key] !== "undefined") { if (typeof token_data[key] !== 'undefined') {
return tokenData[key]; return token_data[key];
} }
return null; return null;
@@ -118,22 +117,22 @@ export default () => {
* @param {String} key * @param {String} key
* @param {*} value * @param {*} value
*/ */
set: (key, value) => { set: function (key, value) {
tokenData[key] = value; token_data[key] = value;
}, },
/** /**
* @param [defaultValue] * @param [default_value]
* @returns {Integer} * @returns {Integer}
*/ */
getUserId: (defaultValue) => { getUserId: (default_value) => {
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;
} }
return defaultValue || 0; return default_value || 0;
}, }
}; };
return self; return self;

View File

@@ -1,65 +1,53 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; const UserPermission = require('./user_permission');
import now from "./now_helper.js"; const now = require('./now_helper');
import UserPermission from "./user_permission.js";
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted", "is_disabled"];
class User extends Model { class User extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for roles // Default for roles
if (typeof this.roles === "undefined") { if (typeof this.roles === 'undefined') {
this.roles = []; this.roles = [];
} }
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'User';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'user';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "User"; return ['roles'];
} }
static get tableName() { static get relationMappings () {
return "user";
}
static get jsonAttributes() {
return ["roles"];
}
static get relationMappings() {
return { return {
permissions: { permissions: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: UserPermission, modelClass: UserPermission,
join: { join: {
from: "user.id", from: 'user.id',
to: "user_permission.user_id", to: 'user_permission.user_id'
}, }
}, }
}; };
} }
} }
export default User; module.exports = User;

View File

@@ -1,9 +1,9 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import { Model } from "objection"; const db = require('../db');
import db from "../db.js"; const Model = require('objection').Model;
import now from "./now_helper.js"; const now = require('./now_helper');
Model.knex(db); Model.knex(db);
@@ -26,4 +26,4 @@ class UserPermission extends Model {
} }
} }
export default UserPermission; module.exports = UserPermission;

View File

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

View File

@@ -1,49 +1,42 @@
{ {
"name": "nginx-proxy-manager", "name": "nginx-proxy-manager",
"version": "2.0.0", "version": "0.0.0",
"description": "A beautiful interface for creating Nginx endpoints", "description": "A beautiful interface for creating Nginx endpoints",
"author": "Jamie Curnow <jc@jc21.com>", "main": "js/index.js",
"license": "MIT",
"main": "index.js",
"type": "module",
"scripts": {
"lint": "biome lint",
"prettier": "biome format --write .",
"validate-schema": "node validate-schema.js"
},
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.0", "ajv": "^6.12.0",
"ajv": "^8.17.1",
"archiver": "^5.3.0", "archiver": "^5.3.0",
"batchflow": "^0.4.0", "batchflow": "^0.4.0",
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"body-parser": "^1.20.3", "body-parser": "^1.19.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.20.0", "express": "^4.19.2",
"express-fileupload": "^1.1.9", "express-fileupload": "^1.1.9",
"gravatar": "^1.8.0", "gravatar": "^1.8.0",
"json-schema-ref-parser": "^8.0.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"knex": "2.4.2", "knex": "2.4.2",
"liquidjs": "10.6.1", "liquidjs": "10.6.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"mysql2": "^3.11.1", "mysql": "^2.18.1",
"node-rsa": "^1.0.8", "node-rsa": "^1.0.8",
"objection": "3.0.1", "objection": "3.0.1",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^8.13.1",
"signale": "1.4.0", "signale": "1.4.0",
"sqlite3": "5.1.6", "sqlite3": "5.1.6",
"temp-write": "^4.0.0" "temp-write": "^4.0.0"
}, },
"devDependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@biomejs/biome": "^2.2.4",
"chalk": "4.1.2",
"nodemon": "^2.0.2"
},
"signale": { "signale": {
"displayDate": true, "displayDate": true,
"displayTimestamp": true "displayTimestamp": true
},
"author": "Jamie Curnow <jc@jc21.com>",
"license": "MIT",
"devDependencies": {
"eslint": "^8.36.0",
"eslint-plugin-align-assignments": "^1.1.2",
"nodemon": "^2.0.2",
"prettier": "^2.0.4"
} }
} }

View File

@@ -0,0 +1,52 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalAuditLog = require('../../internal/audit-log');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/audit-log
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log
*
* Retrieve all logs
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalAuditLog.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,51 @@
const express = require('express');
const pjson = require('../../package.json');
const error = require('../../lib/error');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* Health Check
* GET /api
*/
router.get('/', (req, res/*, next*/) => {
let version = pjson.version.split('-').shift().split('.');
res.status(200).send({
status: 'OK',
version: {
major: parseInt(version.shift(), 10),
minor: parseInt(version.shift(), 10),
revision: parseInt(version.shift(), 10)
}
});
});
router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens'));
router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports'));
router.use('/settings', require('./settings'));
router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts'));
router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts'));
router.use('/nginx/dead-hosts', require('./nginx/dead_hosts'));
router.use('/nginx/streams', require('./nginx/streams'));
router.use('/nginx/access-lists', require('./nginx/access_lists'));
router.use('/nginx/certificates', require('./nginx/certificates'));
/**
* API 404 for all other routes
*
* ALL /api/*
*/
router.all(/(.+)/, function (req, res, next) {
req.params.page = req.params['0'];
next(new error.ItemNotFoundError(req.params.page));
});
module.exports = router;

View File

@@ -0,0 +1,148 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalAccessList = require('../../../internal/access-list');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/access-lists
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists
*
* Retrieve all access-lists
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalAccessList.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/access-lists
*
* Create a new access-list
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/access-lists#/links/1/schema'}, req.body)
.then((payload) => {
return internalAccessList.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific access-list
*
* /api/nginx/access-lists/123
*/
router
.route('/:list_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists/123
*
* Retrieve a specific access-list
*/
.get((req, res, next) => {
validator({
required: ['list_id'],
additionalProperties: false,
properties: {
list_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
list_id: req.params.list_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalAccessList.get(res.locals.access, {
id: parseInt(data.list_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/access-lists/123
*
* Update and existing access-list
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.list_id, 10);
return internalAccessList.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/access-lists/123
*
* Delete and existing access-list
*/
.delete((req, res, next) => {
internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,299 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalCertificate = require('../../../internal/certificate');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/certificates
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates
*
* Retrieve all certificates
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalCertificate.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/certificates
*
* Create a new certificate
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body)
.then((payload) => {
req.setTimeout(900000); // 15 minutes timeout
return internalCertificate.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Test HTTP challenge for domains
*
* /api/nginx/certificates/test-http
*/
router
.route('/test-http')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/test-http
*
* Test HTTP challenge for domains
*/
.get((req, res, next) => {
internalCertificate.testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains))
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Specific certificate
*
* /api/nginx/certificates/123
*/
router
.route('/:certificate_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123
*
* Retrieve a specific certificate
*/
.get((req, res, next) => {
validator({
required: ['certificate_id'],
additionalProperties: false,
properties: {
certificate_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
certificate_id: req.params.certificate_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalCertificate.get(res.locals.access, {
id: parseInt(data.certificate_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/certificates/123
*
* Update and existing certificate
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/certificates#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.certificate_id, 10);
return internalCertificate.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/certificates/123
*
* Update and existing certificate
*/
.delete((req, res, next) => {
internalCertificate.delete(res.locals.access, {id: parseInt(req.params.certificate_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Upload Certs
*
* /api/nginx/certificates/123/upload
*/
router
.route('/:certificate_id/upload')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/upload
*
* Upload certificates
*/
.post((req, res, next) => {
if (!req.files) {
res.status(400)
.send({error: 'No files were uploaded'});
} else {
internalCertificate.upload(res.locals.access, {
id: parseInt(req.params.certificate_id, 10),
files: req.files
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
}
});
/**
* Renew LE Certs
*
* /api/nginx/certificates/123/renew
*/
router
.route('/:certificate_id/renew')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/renew
*
* Renew certificate
*/
.post((req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout
internalCertificate.renew(res.locals.access, {
id: parseInt(req.params.certificate_id, 10)
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Download LE Certs
*
* /api/nginx/certificates/123/download
*/
router
.route('/:certificate_id/download')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123/download
*
* Renew certificate
*/
.get((req, res, next) => {
internalCertificate.download(res.locals.access, {
id: parseInt(req.params.certificate_id, 10)
})
.then((result) => {
res.status(200)
.download(result.fileName);
})
.catch(next);
});
/**
* Validate Certs before saving
*
* /api/nginx/certificates/validate
*/
router
.route('/validate')
.options((req, 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);
}
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalDeadHost = require('../../../internal/dead-host');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/dead-hosts
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts
*
* Retrieve all dead-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalDeadHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/dead-hosts
*
* Create a new dead-host
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/dead-hosts#/links/1/schema'}, req.body)
.then((payload) => {
return internalDeadHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific dead-host
*
* /api/nginx/dead-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts/123
*
* Retrieve a specific dead-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalDeadHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/dead-hosts/123
*
* Update and existing dead-host
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalDeadHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/dead-hosts/123
*
* Update and existing dead-host
*/
.delete((req, res, next) => {
internalDeadHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable dead-host
*
* /api/nginx/dead-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/enable
*/
.post((req, res, next) => {
internalDeadHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable dead-host
*
* /api/nginx/dead-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/disable
*/
.post((req, res, next) => {
internalDeadHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalProxyHost = require('../../../internal/proxy-host');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/proxy-hosts
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts
*
* Retrieve all proxy-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalProxyHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/proxy-hosts
*
* Create a new proxy-host
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/proxy-hosts#/links/1/schema'}, req.body)
.then((payload) => {
return internalProxyHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific proxy-host
*
* /api/nginx/proxy-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts/123
*
* Retrieve a specific proxy-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalProxyHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/proxy-hosts#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalProxyHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.delete((req, res, next) => {
internalProxyHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable proxy-host
*
* /api/nginx/proxy-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/enable
*/
.post((req, res, next) => {
internalProxyHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable proxy-host
*
* /api/nginx/proxy-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/disable
*/
.post((req, res, next) => {
internalProxyHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalRedirectionHost = require('../../../internal/redirection-host');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/redirection-hosts
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/redirection-hosts
*
* Retrieve all redirection-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalRedirectionHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/redirection-hosts
*
* Create a new redirection-host
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/redirection-hosts#/links/1/schema'}, req.body)
.then((payload) => {
return internalRedirectionHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific redirection-host
*
* /api/nginx/redirection-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/redirection-hosts/123
*
* Retrieve a specific redirection-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalRedirectionHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/redirection-hosts/123
*
* Update and existing redirection-host
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/redirection-hosts#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalRedirectionHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/redirection-hosts/123
*
* Update and existing redirection-host
*/
.delete((req, res, next) => {
internalRedirectionHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable redirection-host
*
* /api/nginx/redirection-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/redirection-hosts/123/enable
*/
.post((req, res, next) => {
internalRedirectionHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable redirection-host
*
* /api/nginx/redirection-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/redirection-hosts/123/disable
*/
.post((req, res, next) => {
internalRedirectionHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalStream = require('../../../internal/stream');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/streams
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/streams
*
* Retrieve all streams
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalStream.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/streams
*
* Create a new stream
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/streams#/links/1/schema'}, req.body)
.then((payload) => {
return internalStream.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific stream
*
* /api/nginx/streams/123
*/
router
.route('/:stream_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/streams/123
*
* Retrieve a specific stream
*/
.get((req, res, next) => {
validator({
required: ['stream_id'],
additionalProperties: false,
properties: {
stream_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
stream_id: req.params.stream_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalStream.get(res.locals.access, {
id: parseInt(data.stream_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/streams/123
*
* Update and existing stream
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.stream_id, 10);
return internalStream.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/streams/123
*
* Update and existing stream
*/
.delete((req, res, next) => {
internalStream.delete(res.locals.access, {id: parseInt(req.params.stream_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable stream
*
* /api/nginx/streams/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/streams/123/enable
*/
.post((req, res, next) => {
internalStream.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable stream
*
* /api/nginx/streams/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/streams/123/disable
*/
.post((req, res, next) => {
internalStream.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,29 @@
const express = require('express');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalReport = require('../../internal/report');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/hosts')
.options((req, res) => {
res.sendStatus(204);
})
/**
* GET /reports/hosts
*/
.get(jwtdecode(), (req, res, next) => {
internalReport.getHostsReport(res.locals.access)
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,36 @@
const express = require('express');
const swaggerJSON = require('../../doc/api.swagger.json');
const PACKAGE = require('../../package.json');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
/**
* GET /schema
*/
.get((req, res/*, next*/) => {
let proto = req.protocol;
if (typeof req.headers['x-forwarded-proto'] !== 'undefined' && 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);
});
module.exports = router;

View File

@@ -0,0 +1,96 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalSetting = require('../../internal/setting');
const apiValidator = require('../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/settings
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/settings
*
* Retrieve all settings
*/
.get((req, res, next) => {
internalSetting.getAll(res.locals.access)
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
});
/**
* Specific setting
*
* /api/settings/something
*/
router
.route('/:setting_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /settings/something
*
* Retrieve a specific setting
*/
.get((req, res, next) => {
validator({
required: ['setting_id'],
additionalProperties: false,
properties: {
setting_id: {
$ref: 'definitions#/definitions/setting_id'
}
}
}, {
setting_id: req.params.setting_id
})
.then((data) => {
return internalSetting.get(res.locals.access, {
id: data.setting_id
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/settings/something
*
* Update and existing setting
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/settings#/links/1/schema'}, req.body)
.then((payload) => {
payload.id = req.params.setting_id;
return internalSetting.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,54 @@
const express = require('express');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalToken = require('../../internal/token');
const apiValidator = require('../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
/**
* GET /tokens
*
* Get a new Token, given they already have a token they want to refresh
* We also piggy back on to this method, allowing admins to get tokens
* for services like Job board and Worker.
*/
.get(jwtdecode(), (req, res, next) => {
internalToken.getFreshToken(res.locals.access, {
expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null),
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
})
/**
* POST /tokens
*
* Create a new Token
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/tokens#/links/0/schema'}, req.body)
.then((payload) => {
return internalToken.getTokenFromEmail(payload);
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
});
module.exports = router;

239
backend/routes/api/users.js Normal file
View File

@@ -0,0 +1,239 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const userIdFromMe = require('../../lib/express/user-id-from-me');
const internalUser = require('../../internal/user');
const apiValidator = require('../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/users
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/users
*
* Retrieve all users
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalUser.getAll(res.locals.access, data.expand, data.query);
})
.then((users) => {
res.status(200)
.send(users);
})
.catch(next);
})
/**
* POST /api/users
*
* Create a new User
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/1/schema'}, req.body)
.then((payload) => {
return internalUser.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific user
*
* /api/users/123
*/
router
.route('/:user_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* GET /users/123 or /users/me
*
* Retrieve a specific user
*/
.get((req, res, next) => {
validator({
required: ['user_id'],
additionalProperties: false,
properties: {
user_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
user_id: req.params.user_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalUser.get(res.locals.access, {
id: data.user_id,
expand: data.expand,
omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id)
});
})
.then((user) => {
res.status(200)
.send(user);
})
.catch(next);
})
/**
* PUT /api/users/123
*
* Update and existing user
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/users/123
*
* Update and existing user
*/
.delete((req, res, next) => {
internalUser.delete(res.locals.access, {id: req.params.user_id})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Specific user auth
*
* /api/users/123/auth
*/
router
.route('/:user_id/auth')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* PUT /api/users/123/auth
*
* Update password for a user
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/4/schema'}, req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.setPassword(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific user permissions
*
* /api/users/123/permissions
*/
router
.route('/:user_id/permissions')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* PUT /api/users/123/permissions
*
* Set some or all permissions for a user
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.setPermissions(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific user login as
*
* /api/users/123/login
*/
router
.route('/:user_id/login')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/users/123/login
*
* Log in as a user
*/
.post((req, res, next) => {
internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -1,55 +0,0 @@
import express from "express";
import internalAuditLog from "../internal/audit-log.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import validator from "../lib/validator/index.js";
import { express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/audit-log
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log
*
* Retrieve all logs
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
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,
},
);
const rows = await internalAuditLog.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

View File

@@ -1,66 +0,0 @@
import express from "express";
import errs from "../lib/error.js";
import pjson from "../package.json" with { type: "json" };
import { isSetup } from "../setup.js";
import auditLogRoutes from "./audit-log.js";
import accessListsRoutes from "./nginx/access_lists.js";
import certificatesHostsRoutes from "./nginx/certificates.js";
import deadHostsRoutes from "./nginx/dead_hosts.js";
import proxyHostsRoutes from "./nginx/proxy_hosts.js";
import redirectionHostsRoutes from "./nginx/redirection_hosts.js";
import streamsRoutes from "./nginx/streams.js";
import reportsRoutes from "./reports.js";
import schemaRoutes from "./schema.js";
import settingsRoutes from "./settings.js";
import tokensRoutes from "./tokens.js";
import usersRoutes from "./users.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* Health Check
* GET /api
*/
router.get("/", async (_, res /*, next*/) => {
const version = pjson.version.split("-").shift().split(".");
const setup = await isSetup();
res.status(200).send({
status: "OK",
setup,
version: {
major: Number.parseInt(version.shift(), 10),
minor: Number.parseInt(version.shift(), 10),
revision: Number.parseInt(version.shift(), 10),
},
});
});
router.use("/schema", schemaRoutes);
router.use("/tokens", tokensRoutes);
router.use("/users", usersRoutes);
router.use("/audit-log", auditLogRoutes);
router.use("/reports", reportsRoutes);
router.use("/settings", settingsRoutes);
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
router.use("/nginx/dead-hosts", deadHostsRoutes);
router.use("/nginx/streams", streamsRoutes);
router.use("/nginx/access-lists", accessListsRoutes);
router.use("/nginx/certificates", certificatesHostsRoutes);
/**
* API 404 for all other routes
*
* ALL /api/*
*/
router.all(/(.+)/, (req, _, next) => {
req.params.page = req.params["0"];
next(new errs.ItemNotFoundError(req.params.page));
});
export default router;

View File

@@ -1,155 +0,0 @@
import express from "express";
import internalAccessList from "../../internal/access-list.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/access-lists
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists
*
* Retrieve all access-lists
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
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,
},
);
const rows = await internalAccessList.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/access-lists
*
* Create a new access-list
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body);
const result = await internalAccessList.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific access-list
*
* /api/nginx/access-lists/123
*/
router
.route("/:list_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists/123
*
* Retrieve a specific access-list
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["list_id"],
additionalProperties: false,
properties: {
list_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
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),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/access-lists/123
*
* Update and existing access-list
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body);
payload.id = Number.parseInt(req.params.list_id, 10);
const result = await internalAccessList.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/access-lists/123
*
* Delete and existing access-list
*/
.delete(async (req, res, next) => {
try {
const result = await internalAccessList.delete(res.locals.access, {
id: Number.parseInt(req.params.list_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

View File

@@ -1,303 +0,0 @@
import express from "express";
import internalCertificate from "../../internal/certificate.js";
import errs from "../../lib/error.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/certificates
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates
*
* Retrieve all certificates
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
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,
},
);
const rows = await internalCertificate.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/certificates
*
* Create a new certificate
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body);
req.setTimeout(900000); // 15 minutes timeout
const result = await internalCertificate.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Test HTTP challenge for domains
*
* /api/nginx/certificates/test-http
*/
router
.route("/test-http")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/test-http
*
* Test HTTP challenge for domains
*/
.get(async (req, res, next) => {
if (req.query.domains === undefined) {
next(new errs.ValidationError("Domains are required as query parameters"));
return;
}
try {
const result = await internalCertificate.testHttpsChallenge(
res.locals.access,
JSON.parse(req.query.domains),
);
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific certificate
*
* /api/nginx/certificates/123
*/
router
.route("/:certificate_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123
*
* Retrieve a specific certificate
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["certificate_id"],
additionalProperties: false,
properties: {
certificate_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
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),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/certificates/123
*
* Update and existing certificate
*/
.delete(async (req, res, next) => {
try {
const result = await internalCertificate.delete(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Upload Certs
*
* /api/nginx/certificates/123/upload
*/
router
.route("/:certificate_id/upload")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/upload
*
* Upload certificates
*/
.post(async (req, res, next) => {
if (!req.files) {
res.status(400).send({ error: "No files were uploaded" });
return;
}
try {
const result = await internalCertificate.upload(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
files: req.files,
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Renew LE Certs
*
* /api/nginx/certificates/123/renew
*/
router
.route("/:certificate_id/renew")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/renew
*
* Renew certificate
*/
.post(async (req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout
try {
const result = await internalCertificate.renew(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Download LE Certs
*
* /api/nginx/certificates/123/download
*/
router
.route("/:certificate_id/download")
.options((_req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123/download
*
* Renew certificate
*/
.get(async (req, res, next) => {
try {
const result = await internalCertificate.download(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).download(result.fileName);
} 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);
}
});
export default router;

View File

@@ -1,207 +0,0 @@
import express from "express";
import internalDeadHost from "../../internal/dead-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/dead-hosts
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts
*
* Retrieve all dead-hosts
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
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,
},
);
const rows = await internalDeadHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/dead-hosts
*
* Create a new dead-host
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts", "post"), req.body);
const result = await internalDeadHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific dead-host
*
* /api/nginx/dead-hosts/123
*/
router
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts/123
*
* Retrieve a specific dead-host
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
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),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/dead-hosts/123
*
* Update and existing dead-host
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts/{hostID}", "put"), req.body);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalDeadHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/dead-hosts/123
*
* Update and existing dead-host
*/
.delete(async (req, res, next) => {
try {
const result = await internalDeadHost.delete(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Enable dead-host
*
* /api/nginx/dead-hosts/123/enable
*/
router
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/enable
*/
.post(async (req, res, next) => {
try {
const result = await internalDeadHost.enable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Disable dead-host
*
* /api/nginx/dead-hosts/123/disable
*/
router
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/disable
*/
.post((req, res, next) => {
try {
const result = internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) });
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

View File

@@ -1,209 +0,0 @@
import express from "express";
import internalProxyHost from "../../internal/proxy-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/proxy-hosts
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts
*
* Retrieve all proxy-hosts
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
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,
},
);
const rows = await internalProxyHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/proxy-hosts
*
* Create a new proxy-host
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body);
const result = await internalProxyHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific proxy-host
*
* /api/nginx/proxy-hosts/123
*/
router
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts/123
*
* Retrieve a specific proxy-host
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
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),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts/{hostID}", "put"), req.body);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalProxyHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.delete(async (req, res, next) => {
try {
const result = await internalProxyHost.delete(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Enable proxy-host
*
* /api/nginx/proxy-hosts/123/enable
*/
router
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/enable
*/
.post(async (req, res, next) => {
try {
const result = await internalProxyHost.enable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Disable proxy-host
*
* /api/nginx/proxy-hosts/123/disable
*/
router
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/disable
*/
.post(async (req, res, next) => {
try {
const result = await internalProxyHost.disable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

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