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" \ 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 / && 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 # Volumes
VOLUME [ "/data", "/etc/letsencrypt" ] VOLUME [ "/data", "/etc/letsencrypt" ]
CMD [ "/init" ] CMD [ "/init" ]
@ -37,3 +28,14 @@ EXPOSE 9876
HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1 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 ### 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: 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+ - 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 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. 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 ### Running the App

View file

@ -49,16 +49,18 @@
"html-entities": "^1.2.1", "html-entities": "^1.2.1",
"json-schema-ref-parser": "^5.0.3", "json-schema-ref-parser": "^5.0.3",
"jsonwebtoken": "^8.3.0", "jsonwebtoken": "^8.3.0",
"knex": "^0.15.2", "knex": "^0.20.0",
"liquidjs": "^5.1.1", "liquidjs": "^5.1.1",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"moment": "^2.22.2", "moment": "^2.22.2",
"mysql": "^2.15.0", "mysql2": "^1.7.0",
"node-rsa": "^1.0.0", "node-rsa": "^1.0.0",
"objection": "^1.1.10", "objection": "^1.1.10",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^7.12.1",
"restler": "^3.4.0", "restler": "^3.4.0",
"signale": "^1.2.1", "signale": "^1.2.1",
"sqlite3": "^4.1.1",
"temp-write": "^3.4.0", "temp-write": "^3.4.0",
"unix-timestamp": "^0.2.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'); 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 = { function generateDbConfig() {
client: config.database.engine, if (config.database.engine === 'knex-native') {
connection: { return config.database.knex;
host: config.database.host, } else
user: config.database.user, return {
password: config.database.password, client: config.database.engine,
database: config.database.name, connection: {
port: config.database.port host: config.database.host,
}, user: config.database.user,
migrations: { password: config.database.password,
tableName: 'migrations' database: config.database.name,
} port: config.database.port
}; },
migrations: {
tableName: 'migrations'
}
};
}
let data = generateDbConfig();
if (typeof config.database.version !== 'undefined') { if (typeof config.database.version !== 'undefined') {
data.version = config.database.version; data.version = config.database.version;

View file

@ -22,22 +22,6 @@ exports.up = function (knex/*, Promise*/) {
}) })
.then(() => { .then(() => {
logger.info('[' + migrate_name + '] setting Table created'); 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 Model = require('objection').Model;
const User = require('./user'); const User = require('./user');
const AccessListAuth = require('./access_list_auth'); const AccessListAuth = require('./access_list_auth');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
class AccessList extends Model { class AccessList extends Model {
$beforeInsert () { $beforeInsert () {
this.created_on = Model.raw('NOW()'); this.created_on = now();
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
// Default for meta // Default for meta
if (typeof this.meta === 'undefined') { if (typeof this.meta === 'undefined') {
@ -20,7 +21,7 @@ class AccessList extends Model {
} }
$beforeUpdate () { $beforeUpdate () {
this.modified_on = Model.raw('NOW()'); this.modified_on = now();
} }
static get name () { static get name () {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,10 +5,11 @@ const logger = require('./logger').setup;
const userModel = require('./models/user'); const userModel = require('./models/user');
const userPermissionModel = require('./models/user_permission'); const userPermissionModel = require('./models/user_permission');
const authModel = require('./models/auth'); const authModel = require('./models/auth');
const settingModel = require('./models/setting');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
module.exports = function () { 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 // 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')) { if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) {
logger.info('Creating a new JWT key pair...'); logger.info('Creating a new JWT key pair...');
@ -56,15 +57,15 @@ module.exports = function () {
resolve(); resolve();
} }
}) }
.then(() => {
return userModel function setupDefaultUser() {
.query() (userModel
.select(userModel.raw('COUNT(`id`) as `count`')) .query()
.where('is_deleted', 0) .select(userModel.raw('COUNT(`id`) as `count`'))
.first(); .where('is_deleted', 0)
}) .first()
.then(row => { ).then(row => {
if (!row.count) { if (!row.count) {
// Create a new user and set password // Create a new user and set password
logger.info('Creating a new user: admin@example.com with password: changeme'); logger.info('Creating a new user: admin@example.com with password: changeme');
@ -79,37 +80,71 @@ module.exports = function () {
}; };
return userModel return userModel
.query() .query()
.insertAndFetch(data) .insertAndFetch(data)
.then(user => { .then(user => {
return authModel return authModel
.query() .query()
.insert({ .insert({
user_id: user.id, user_id: user.id,
type: 'password', type: 'password',
secret: 'changeme', secret: 'changeme',
meta: {} meta: {}
}) })
.then(() => { .then(() => {
return userPermissionModel return userPermissionModel
.query() .query()
.insert({ .insert({
user_id: user.id, user_id: user.id,
visibility: 'all', visibility: 'all',
proxy_hosts: 'manage', proxy_hosts: 'manage',
redirection_hosts: 'manage', redirection_hosts: 'manage',
dead_hosts: 'manage', dead_hosts: 'manage',
streams: 'manage', streams: 'manage',
access_lists: 'manage', access_lists: 'manage',
certificates: 'manage' certificates: 'manage'
}); });
}); });
}) })
.then(() => { .then(() => {
logger.info('Initial setup completed'); logger.info('Initial admin setup completed');
}); });
} else if (debug_mode) { } else if (debug_mode) {
logger.debug('Admin user setup not required'); 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();
});
}; };