added experimental multidb support

This commit is contained in:
Gergo Torcsvari 2019-12-15 15:48:13 +01:00
parent 8a9495de18
commit bac0de7300
20 changed files with 213 additions and 114 deletions

7
.dockerignore Normal file
View file

@ -0,0 +1,7 @@
.github/*
.idea/*
config/*
data/*
doc/*
dist/*
node_modules/*

View file

@ -16,15 +16,6 @@ COPY rootfs /
RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.4.0/s6-overlay-amd64.tar.gz" \
&& tar xzf /tmp/s6-overlay-amd64.tar.gz -C /
# App
ENV NODE_ENV=production
ADD dist /app/dist
ADD node_modules /app/node_modules
ADD src/backend /app/src/backend
ADD package.json /app/package.json
ADD knexfile.js /app/knexfile.js
# Volumes
VOLUME [ "/data", "/etc/letsencrypt" ]
CMD [ "/init" ]
@ -37,3 +28,14 @@ EXPOSE 9876
HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1
# App
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm set progress=false && npm config set depth 0 && npm cache clean --force
RUN npm install
ENV NODE_ENV=production
COPY . .
RUN npm run-script build

View file

@ -40,6 +40,7 @@ affect the login and session management of the application. If these keys change
### Database
#### Tested:
This app doesn't come with a database, you have to provide one yourself. Currently only `mysql/mariadb` is supported for the minimum versions:
- MySQL v5.7.8+
@ -48,6 +49,40 @@ This app doesn't come with a database, you have to provide one yourself. Current
It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples
are going to use.
#### Experimental:
The app uses `knex` as its database connector library.
In theory it should work with postgres and sqlite too.
These are not battle-tested, but if you are feel yoursef confident enough you can try to config them!
Example sqlite3 config:
```json
{
"database": {
"engine": "knex-native",
"knex": {
"client": "sqlite3",
"connection": {
"filename": "/app/config/mydb.sqlite"
},
"pool": {
"min": 0,
"max": 1,
"createTimeoutMillis": 3000,
"acquireTimeoutMillis": 31000,
"idleTimeoutMillis": 30000,
"reapIntervalMillis": 1000,
"createRetryIntervalMillis": 100,
"propagateCreateError": false
},
"migrations": {
"tableName": "migrations"
},
"useNullAsDefault": true
}
}
}
```
With using the `knex-native` engine, you can add a [knex config](http://knexjs.org/#Installation-client) under the `knex` key.
### Running the App

View file

@ -49,16 +49,18 @@
"html-entities": "^1.2.1",
"json-schema-ref-parser": "^5.0.3",
"jsonwebtoken": "^8.3.0",
"knex": "^0.15.2",
"knex": "^0.20.0",
"liquidjs": "^5.1.1",
"lodash": "^4.17.10",
"moment": "^2.22.2",
"mysql": "^2.15.0",
"mysql2": "^1.7.0",
"node-rsa": "^1.0.0",
"objection": "^1.1.10",
"path": "^0.12.7",
"pg": "^7.12.1",
"restler": "^3.4.0",
"signale": "^1.2.1",
"sqlite3": "^4.1.1",
"temp-write": "^3.4.0",
"unix-timestamp": "^0.2.0"
},

View file

@ -4,19 +4,27 @@ if (!config.has('database')) {
throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md');
}
let data = {
client: config.database.engine,
connection: {
host: config.database.host,
user: config.database.user,
password: config.database.password,
database: config.database.name,
port: config.database.port
},
migrations: {
tableName: 'migrations'
}
};
function generateDbConfig() {
if (config.database.engine === 'knex-native') {
return config.database.knex;
} else
return {
client: config.database.engine,
connection: {
host: config.database.host,
user: config.database.user,
password: config.database.password,
database: config.database.name,
port: config.database.port
},
migrations: {
tableName: 'migrations'
}
};
}
let data = generateDbConfig();
if (typeof config.database.version !== 'undefined') {
data.version = config.database.version;

View file

@ -32,4 +32,4 @@ exports.up = function (knex/*, Promise*/) {
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
};

View file

@ -22,22 +22,6 @@ exports.up = function (knex/*, Promise*/) {
})
.then(() => {
logger.info('[' + migrate_name + '] setting Table created');
// TODO: add settings
let settingModel = require('../models/setting');
return settingModel
.query()
.insert({
id: 'default-site',
name: 'Default Site',
description: 'What to show when Nginx is hit with an unknown Host',
value: 'congratulations',
meta: {}
});
})
.then(() => {
logger.info('[' + migrate_name + '] Default settings added');
});
};

View file

@ -5,13 +5,14 @@ const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const AccessListAuth = require('./access_list_auth');
const now = require('./now_helper');
Model.knex(db);
class AccessList extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
@ -20,7 +21,7 @@ class AccessList extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
}
static get name () {

View file

@ -3,13 +3,14 @@
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db);
class AccessListAuth extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
@ -18,7 +19,7 @@ class AccessListAuth extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
}
static get name () {

View file

@ -4,13 +4,14 @@
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
class AuditLog extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
@ -19,7 +20,7 @@ class AuditLog extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
}
static get name () {

View file

@ -5,6 +5,7 @@ const bcrypt = require('bcrypt');
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
@ -24,8 +25,8 @@ function encryptPassword () {
class Auth extends Model {
$beforeInsert (queryContext) {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
@ -36,7 +37,7 @@ class Auth extends Model {
}
$beforeUpdate (queryContext) {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
return encryptPassword.apply(this, queryContext);
}

View file

@ -4,17 +4,18 @@
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
class Certificate extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for expires_on
if (typeof this.expires_on === 'undefined') {
this.expires_on = Model.raw('NOW()');
this.expires_on = now();
}
// Default for domain_names
@ -31,7 +32,7 @@ class Certificate extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {

View file

@ -5,13 +5,14 @@ const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
class DeadHost extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === 'undefined') {
@ -27,7 +28,7 @@ class DeadHost extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {

View file

@ -0,0 +1,15 @@
const db = require('../db');
const config = require('config');
const Model = require('objection').Model;
Model.knex(db);
module.exports = function () {
if(config.database.knex && config.database.knex.client === 'sqlite3') {
return Model.raw("date('now')");
} else {
return Model.raw('NOW()');
}
};

View file

@ -6,13 +6,14 @@ const Model = require('objection').Model;
const User = require('./user');
const AccessList = require('./access_list');
const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
class ProxyHost extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === 'undefined') {
@ -28,7 +29,7 @@ class ProxyHost extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {

View file

@ -5,13 +5,14 @@ const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
class RedirectionHost extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === 'undefined') {
@ -27,7 +28,7 @@ class RedirectionHost extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {

View file

@ -4,13 +4,14 @@
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
class Stream extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
@ -19,7 +20,7 @@ class Stream extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
}
static get name () {

View file

@ -4,13 +4,14 @@
const db = require('../db');
const Model = require('objection').Model;
const UserPermission = require('./user_permission');
const now = require('./now_helper');
Model.knex(db);
class User extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
// Default for roles
if (typeof this.roles === 'undefined') {
@ -19,7 +20,7 @@ class User extends Model {
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
}
static get name () {

View file

@ -3,17 +3,18 @@
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db);
class UserPermission extends Model {
$beforeInsert () {
this.created_on = Model.raw('NOW()');
this.modified_on = Model.raw('NOW()');
this.created_on = now();
this.modified_on = now();
}
$beforeUpdate () {
this.modified_on = Model.raw('NOW()');
this.modified_on = now();
}
static get name () {

View file

@ -5,10 +5,11 @@ const logger = require('./logger').setup;
const userModel = require('./models/user');
const userPermissionModel = require('./models/user_permission');
const authModel = require('./models/auth');
const settingModel = require('./models/setting');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
module.exports = function () {
return new Promise((resolve, reject) => {
function setupJwt(resolve, reject) {
// Now go and check if the jwt gpg keys have been created and if not, create them
if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) {
logger.info('Creating a new JWT key pair...');
@ -56,15 +57,15 @@ module.exports = function () {
resolve();
}
})
.then(() => {
return userModel
.query()
.select(userModel.raw('COUNT(`id`) as `count`'))
.where('is_deleted', 0)
.first();
})
.then(row => {
}
function setupDefaultUser() {
(userModel
.query()
.select(userModel.raw('COUNT(`id`) as `count`'))
.where('is_deleted', 0)
.first()
).then(row => {
if (!row.count) {
// Create a new user and set password
logger.info('Creating a new user: admin@example.com with password: changeme');
@ -79,37 +80,71 @@ module.exports = function () {
};
return userModel
.query()
.insertAndFetch(data)
.then(user => {
return authModel
.query()
.insert({
user_id: user.id,
type: 'password',
secret: 'changeme',
meta: {}
})
.then(() => {
return userPermissionModel
.query()
.insert({
user_id: user.id,
visibility: 'all',
proxy_hosts: 'manage',
redirection_hosts: 'manage',
dead_hosts: 'manage',
streams: 'manage',
access_lists: 'manage',
certificates: 'manage'
});
});
})
.then(() => {
logger.info('Initial setup completed');
});
.query()
.insertAndFetch(data)
.then(user => {
return authModel
.query()
.insert({
user_id: user.id,
type: 'password',
secret: 'changeme',
meta: {}
})
.then(() => {
return userPermissionModel
.query()
.insert({
user_id: user.id,
visibility: 'all',
proxy_hosts: 'manage',
redirection_hosts: 'manage',
dead_hosts: 'manage',
streams: 'manage',
access_lists: 'manage',
certificates: 'manage'
});
});
})
.then(() => {
logger.info('Initial admin setup completed');
});
} else if (debug_mode) {
logger.debug('Admin user setup not required');
}
});
}
function setupDefaultSettings() {
return settingModel
.query()
.select(userModel.raw('COUNT(`id`) as `count`'))
.first()
.then(row => {
if (!row.count) {
settingModel
.query()
.insert({
id: 'default-site',
name: 'Default Site',
description: 'What to show when Nginx is hit with an unknown Host',
value: 'congratulations',
meta: {}
}).then(() => {
logger.info('Default settings added');
})
} if (debug_mode) {
logger.debug('Default setting setup not required');
}
});
}
return new Promise((resolve, reject) => {
return setupJwt(resolve, reject);
}).then(() => {
return setupDefaultUser();
}).then(() => {
return setupDefaultSettings();
});
};