mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-10-25 12:53:34 +00:00
Version 3 starter
This commit is contained in:
@@ -1,534 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const batchflow = require('batchflow');
|
||||
const logger = require('../logger').access;
|
||||
const error = require('../lib/error');
|
||||
const accessListModel = require('../models/access_list');
|
||||
const accessListAuthModel = require('../models/access_list_auth');
|
||||
const accessListClientModel = require('../models/access_list_client');
|
||||
const proxyHostModel = require('../models/proxy_host');
|
||||
const internalAuditLog = require('./audit-log');
|
||||
const internalNginx = require('./nginx');
|
||||
const utils = require('../lib/utils');
|
||||
|
||||
function omissions () {
|
||||
return ['is_deleted'];
|
||||
}
|
||||
|
||||
const internalAccessList = {
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
create: (access, data) => {
|
||||
return access.can('access_lists:create', data)
|
||||
.then((/*access_data*/) => {
|
||||
return accessListModel
|
||||
.query()
|
||||
.omit(omissions())
|
||||
.insertAndFetch({
|
||||
name: data.name,
|
||||
satisfy_any: data.satisfy_any,
|
||||
pass_auth: data.pass_auth,
|
||||
owner_user_id: access.token.getUserId(1)
|
||||
});
|
||||
})
|
||||
.then((row) => {
|
||||
data.id = row.id;
|
||||
|
||||
let promises = [];
|
||||
|
||||
// Now add the items
|
||||
data.items.map((item) => {
|
||||
promises.push(accessListAuthModel
|
||||
.query()
|
||||
.insert({
|
||||
access_list_id: row.id,
|
||||
username: item.username,
|
||||
password: item.password
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Now add the clients
|
||||
if (typeof data.clients !== 'undefined' && data.clients) {
|
||||
data.clients.map((client) => {
|
||||
promises.push(accessListClientModel
|
||||
.query()
|
||||
.insert({
|
||||
access_list_id: row.id,
|
||||
address: client.address,
|
||||
directive: client.directive
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return internalAccessList.build(row)
|
||||
.then(() => {
|
||||
if (row.proxy_host_count) {
|
||||
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Add to audit log
|
||||
return internalAuditLog.add(access, {
|
||||
action: 'created',
|
||||
object_type: 'access-list',
|
||||
object_id: row.id,
|
||||
meta: internalAccessList.maskItems(data)
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return internalAccessList.maskItems(row);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Integer} data.id
|
||||
* @param {String} [data.name]
|
||||
* @param {String} [data.items]
|
||||
* @return {Promise}
|
||||
*/
|
||||
update: (access, data) => {
|
||||
return access.can('access_lists:update', data.id)
|
||||
.then((/*access_data*/) => {
|
||||
return internalAccessList.get(access, {id: data.id});
|
||||
})
|
||||
.then((row) => {
|
||||
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);
|
||||
}
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
// patch name if specified
|
||||
if (typeof data.name !== 'undefined' && data.name) {
|
||||
return accessListModel
|
||||
.query()
|
||||
.where({id: data.id})
|
||||
.patch({
|
||||
name: data.name,
|
||||
satisfy_any: data.satisfy_any,
|
||||
pass_auth: data.pass_auth,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Check for items and add/update/remove them
|
||||
if (typeof data.items !== 'undefined' && data.items) {
|
||||
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.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(() => {
|
||||
return internalAccessList.maskItems(row);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Integer} data.id
|
||||
* @param {Array} [data.expand]
|
||||
* @param {Array} [data.omit]
|
||||
* @param {Boolean} [skip_masking]
|
||||
* @return {Promise}
|
||||
*/
|
||||
get: (access, data, skip_masking) => {
|
||||
if (typeof data === 'undefined') {
|
||||
data = {};
|
||||
}
|
||||
|
||||
return access.can('access_lists:get', data.id)
|
||||
.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)
|
||||
.andWhere('access_list.id', data.id)
|
||||
.allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]')
|
||||
.omit(['access_list.is_deleted'])
|
||||
.first();
|
||||
|
||||
if (access_data.permission_visibility !== 'all') {
|
||||
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
|
||||
}
|
||||
|
||||
// Custom omissions
|
||||
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||
query.omit(data.omit);
|
||||
}
|
||||
|
||||
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||
query.eager('[' + data.expand.join(', ') + ']');
|
||||
}
|
||||
|
||||
return query;
|
||||
})
|
||||
.then((row) => {
|
||||
if (row) {
|
||||
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
|
||||
row = internalAccessList.maskItems(row);
|
||||
}
|
||||
|
||||
return _.omit(row, omissions());
|
||||
} else {
|
||||
throw new error.ItemNotFoundError(data.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Integer} data.id
|
||||
* @param {String} [data.reason]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
delete: (access, data) => {
|
||||
return access.can('access_lists:delete', data.id)
|
||||
.then(() => {
|
||||
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
|
||||
})
|
||||
.then((row) => {
|
||||
if (!row) {
|
||||
throw new error.ItemNotFoundError(data.id);
|
||||
}
|
||||
|
||||
// 1. update row to be deleted
|
||||
// 2. update any proxy hosts that were using it (ignoring permissions)
|
||||
// 3. reconfigure those hosts
|
||||
// 4. audit log
|
||||
|
||||
// 1. update row to be deleted
|
||||
return accessListModel
|
||||
.query()
|
||||
.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
|
||||
|
||||
// set the access_list_id to zero for these items
|
||||
row.proxy_hosts.map(function (val, idx) {
|
||||
row.proxy_hosts[idx].access_list_id = 0;
|
||||
});
|
||||
|
||||
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
|
||||
})
|
||||
.then(() => {
|
||||
return internalNginx.reload();
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// delete the htpasswd file
|
||||
let htpasswd_file = internalAccessList.getFilename(row);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(htpasswd_file);
|
||||
} catch (err) {
|
||||
// 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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* All Lists
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {Array} [expand]
|
||||
* @param {String} [search_query]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAll: (access, expand, search_query) => {
|
||||
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')
|
||||
.omit(['access_list.is_deleted'])
|
||||
.allowEager('[owner,items,clients]')
|
||||
.orderBy('access_list.name', 'ASC');
|
||||
|
||||
if (access_data.permission_visibility !== 'all') {
|
||||
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
|
||||
}
|
||||
|
||||
// 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.eager('[' + expand.join(', ') + ']');
|
||||
}
|
||||
|
||||
return query;
|
||||
})
|
||||
.then((rows) => {
|
||||
if (rows) {
|
||||
rows.map(function (row, idx) {
|
||||
if (typeof row.items !== 'undefined' && row.items) {
|
||||
rows[idx] = internalAccessList.maskItems(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Report use
|
||||
*
|
||||
* @param {Integer} user_id
|
||||
* @param {String} visibility
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCount: (user_id, visibility) => {
|
||||
let query = accessListModel
|
||||
.query()
|
||||
.count('id as count')
|
||||
.where('is_deleted', 0);
|
||||
|
||||
if (visibility !== 'all') {
|
||||
query.andWhere('owner_user_id', user_id);
|
||||
}
|
||||
|
||||
return query.first()
|
||||
.then((row) => {
|
||||
return parseInt(row.count, 10);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} list
|
||||
* @returns {Object}
|
||||
*/
|
||||
maskItems: (list) => {
|
||||
if (list && typeof list.items !== 'undefined') {
|
||||
list.items.map(function (val, idx) {
|
||||
let repeat_for = 8;
|
||||
let first_char = '*';
|
||||
|
||||
if (typeof val.password !== 'undefined' && val.password) {
|
||||
repeat_for = val.password.length - 1;
|
||||
first_char = val.password.charAt(0);
|
||||
}
|
||||
|
||||
list.items[idx].hint = first_char + ('*').repeat(repeat_for);
|
||||
list.items[idx].password = '';
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} list
|
||||
* @param {Integer} list.id
|
||||
* @returns {String}
|
||||
*/
|
||||
getFilename: (list) => {
|
||||
return '/data/access/' + list.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} list
|
||||
* @param {Integer} list.id
|
||||
* @param {String} list.name
|
||||
* @param {Array} list.items
|
||||
* @returns {Promise}
|
||||
*/
|
||||
build: (list) => {
|
||||
logger.info('Building Access file #' + list.id + ' for: ' + list.name);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let htpasswd_file = internalAccessList.getFilename(list);
|
||||
|
||||
// 1. remove any existing access file
|
||||
try {
|
||||
fs.unlinkSync(htpasswd_file);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// 2. create empty access file
|
||||
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);
|
||||
|
||||
utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
|
||||
.then((/*result*/) => {
|
||||
next();
|
||||
})
|
||||
.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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = internalAccessList;
|
||||
25
backend/internal/api/context/context.go
Normal file
25
backend/internal/api/context/context.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package context
|
||||
|
||||
var (
|
||||
// BodyCtxKey is the name of the Body value on the context
|
||||
BodyCtxKey = &contextKey{"Body"}
|
||||
// UserIDCtxKey is the name of the UserID value on the context
|
||||
UserIDCtxKey = &contextKey{"UserID"}
|
||||
// FiltersCtxKey is the name of the Filters value on the context
|
||||
FiltersCtxKey = &contextKey{"Filters"}
|
||||
// PrettyPrintCtxKey is the name of the pretty print context
|
||||
PrettyPrintCtxKey = &contextKey{"Pretty"}
|
||||
// ExpansionCtxKey is the name of the expansion context
|
||||
ExpansionCtxKey = &contextKey{"Expansion"}
|
||||
)
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string {
|
||||
return "context value: " + k.name
|
||||
}
|
||||
208
backend/internal/api/filters/helpers.go
Normal file
208
backend/internal/api/filters/helpers.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewFilterSchema is the main method to specify a new Filter Schema for use in Middleware
|
||||
func NewFilterSchema(fieldSchemas []string) string {
|
||||
return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", "))
|
||||
}
|
||||
|
||||
// BoolFieldSchema returns the Field Schema for a Boolean accepted value field
|
||||
func BoolFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
%s,
|
||||
{
|
||||
"type": "array",
|
||||
"items": %s
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, boolModifiers, filterBool, filterBool)
|
||||
}
|
||||
|
||||
// IntFieldSchema returns the Field Schema for a Integer accepted value field
|
||||
func IntFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
// StringFieldSchema returns the Field Schema for a String accepted value field
|
||||
func StringFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
%s,
|
||||
{
|
||||
"type": "array",
|
||||
"items": %s
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, stringModifiers, filterString, filterString)
|
||||
}
|
||||
|
||||
// RegexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
|
||||
func RegexFieldSchema(fieldName string, regex string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "%s"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "%s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, stringModifiers, regex, regex)
|
||||
}
|
||||
|
||||
// DateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
|
||||
func DateFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
// DateTimeFieldSchema returns the Field Schema for a String accepted value field matching a Date format
|
||||
// 2020-03-01T10:30:00+10:00
|
||||
func DateTimeFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
const allModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
|
||||
}`
|
||||
|
||||
const boolModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not)$"
|
||||
}`
|
||||
|
||||
const stringModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
|
||||
}`
|
||||
|
||||
const filterBool = `{
|
||||
"type": "string",
|
||||
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
|
||||
}`
|
||||
|
||||
const filterString = `{
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}`
|
||||
|
||||
const baseFilterSchema = `{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
%s
|
||||
]
|
||||
}
|
||||
}`
|
||||
54
backend/internal/api/handler/auth.go
Normal file
54
backend/internal/api/handler/auth.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/entity/auth"
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
// SetAuth ...
|
||||
// Route: POST /users/:userID/auth
|
||||
func SetAuth() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
// TODO:
|
||||
// delete old auth for user
|
||||
// test endpoint
|
||||
|
||||
var newAuth auth.Model
|
||||
err := json.Unmarshal(bodyBytes, &newAuth)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
userID, _, userIDErr := getUserIDFromRequest(r)
|
||||
if userIDErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
newAuth.UserID = userID
|
||||
if newAuth.Type == auth.TypePassword {
|
||||
err := newAuth.SetPassword(newAuth.Secret)
|
||||
if err != nil {
|
||||
logger.Error("SetPasswordError", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = newAuth.Save(); err != nil {
|
||||
logger.Error("AuthSaveError", err)
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
||||
return
|
||||
}
|
||||
|
||||
newAuth.Secret = ""
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newAuth)
|
||||
}
|
||||
}
|
||||
126
backend/internal/api/handler/certificate_authorities.go
Normal file
126
backend/internal/api/handler/certificate_authorities.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
)
|
||||
|
||||
// GetCertificateAuthorities will return a list of Certificate Authorities
|
||||
// Route: GET /certificate-authorities
|
||||
func GetCertificateAuthorities() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
certificates, err := certificateauthority.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificateAuthority will return a single Certificate Authority
|
||||
// Route: GET /certificate-authorities/{caID}
|
||||
func GetCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var caID int
|
||||
if caID, err = getURLParamInt(r, "caID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := certificateauthority.GetByID(caID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCertificateAuthority will create a Certificate Authority
|
||||
// Route: POST /certificate-authorities
|
||||
func CreateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newCA certificateauthority.Model
|
||||
err := json.Unmarshal(bodyBytes, &newCA)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newCA.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate Authority: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newCA)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCertificateAuthority ...
|
||||
// Route: PUT /certificate-authorities/{caID}
|
||||
func UpdateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var caID int
|
||||
if caID, err = getURLParamInt(r, "caID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
ca, err := certificateauthority.GetByID(caID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &ca)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ca.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, ca)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteCertificateAuthority ...
|
||||
// Route: DELETE /certificate-authorities/{caID}
|
||||
func DeleteCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var caID int
|
||||
if caID, err = getURLParamInt(r, "caID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := certificateauthority.GetByID(caID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
145
backend/internal/api/handler/certificates.go
Normal file
145
backend/internal/api/handler/certificates.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/api/schema"
|
||||
"npm/internal/entity/certificate"
|
||||
)
|
||||
|
||||
// GetCertificates will return a list of Certificates
|
||||
// Route: GET /certificates
|
||||
func GetCertificates() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificate will return a single Certificate
|
||||
// Route: GET /certificates/{certificateID}
|
||||
func GetCertificate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var certificateID int
|
||||
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := certificate.GetByID(certificateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCertificate will create a Certificate
|
||||
// Route: POST /certificates
|
||||
func CreateCertificate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newCertificate certificate.Model
|
||||
err := json.Unmarshal(bodyBytes, &newCertificate)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newCertificate.UserID = userID
|
||||
|
||||
if err = newCertificate.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newCertificate)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCertificate ...
|
||||
// Route: PUT /certificates/{certificateID}
|
||||
func UpdateCertificate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var certificateID int
|
||||
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
certificateObject, err := certificate.GetByID(certificateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
|
||||
// This is a special endpoint, as it needs to verify the schema payload
|
||||
// based on the certificate type, without being given a type in the payload.
|
||||
// The middleware would normally handle this.
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), schema.UpdateCertificate(certificateObject.Type), bodyBytes)
|
||||
if jsonErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(schemaErrors) > 0 {
|
||||
h.ResultSchemaErrorJSON(w, r, schemaErrors)
|
||||
return
|
||||
}
|
||||
|
||||
err := json.Unmarshal(bodyBytes, &certificateObject)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = certificateObject.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, certificateObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteCertificate ...
|
||||
// Route: DELETE /certificates/{certificateID}
|
||||
func DeleteCertificate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var certificateID int
|
||||
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := certificate.GetByID(certificateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
15
backend/internal/api/handler/config.go
Normal file
15
backend/internal/api/handler/config.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
// Config returns the entire configuration, for debug purposes
|
||||
// Route: GET /config
|
||||
func Config() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, config.Configuration)
|
||||
}
|
||||
}
|
||||
129
backend/internal/api/handler/dns_providers.go
Normal file
129
backend/internal/api/handler/dns_providers.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
)
|
||||
|
||||
// GetDNSProviders will return a list of DNS Providers
|
||||
// Route: GET /dns-providers
|
||||
func GetDNSProviders() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
items, err := dnsprovider.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDNSProvider will return a single DNS Provider
|
||||
// Route: GET /dns-providers/{providerID}
|
||||
func GetDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var providerID int
|
||||
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := dnsprovider.GetByID(providerID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDNSProvider will create a DNS Provider
|
||||
// Route: POST /dns-providers
|
||||
func CreateDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newItem dnsprovider.Model
|
||||
err := json.Unmarshal(bodyBytes, &newItem)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newItem.UserID = userID
|
||||
|
||||
if err = newItem.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save DNS Provider: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDNSProvider ...
|
||||
// Route: PUT /dns-providers/{providerID}
|
||||
func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var providerID int
|
||||
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := dnsprovider.GetByID(providerID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &item)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = item.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteDNSProvider ...
|
||||
// Route: DELETE /dns-providers/{providerID}
|
||||
func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var providerID int
|
||||
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := dnsprovider.GetByID(providerID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
31
backend/internal/api/handler/health.go
Normal file
31
backend/internal/api/handler/health.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
type healthCheckResponse struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
Healthy bool `json:"healthy"`
|
||||
IsSetup bool `json:"setup"`
|
||||
ErrorReporting bool `json:"error_reporting"`
|
||||
}
|
||||
|
||||
// Health returns the health of the api
|
||||
// Route: GET /health
|
||||
func Health() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
health := healthCheckResponse{
|
||||
Version: config.Version,
|
||||
Commit: config.Commit,
|
||||
Healthy: true,
|
||||
IsSetup: config.IsSetup,
|
||||
ErrorReporting: config.ErrorReporting,
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, health)
|
||||
}
|
||||
}
|
||||
151
backend/internal/api/handler/helpers.go
Normal file
151
backend/internal/api/handler/helpers.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"npm/internal/model"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
const defaultLimit = 10
|
||||
|
||||
func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
|
||||
var pageInfo model.PageInfo
|
||||
var err error
|
||||
|
||||
pageInfo.FromDate, pageInfo.ToDate, err = getDateRanges(r)
|
||||
if err != nil {
|
||||
return pageInfo, err
|
||||
}
|
||||
|
||||
pageInfo.Offset, pageInfo.Limit, err = getPagination(r)
|
||||
if err != nil {
|
||||
return pageInfo, err
|
||||
}
|
||||
|
||||
pageInfo.Sort = getSortParameter(r)
|
||||
|
||||
return pageInfo, nil
|
||||
}
|
||||
|
||||
func getDateRanges(r *http.Request) (time.Time, time.Time, error) {
|
||||
queryValues := r.URL.Query()
|
||||
from := queryValues.Get("from")
|
||||
fromDate := time.Now().AddDate(0, -1, 0) // 1 month ago by default
|
||||
to := queryValues.Get("to")
|
||||
toDate := time.Now()
|
||||
|
||||
if from != "" {
|
||||
var fromErr error
|
||||
fromDate, fromErr = time.Parse(time.RFC3339, from)
|
||||
if fromErr != nil {
|
||||
return fromDate, toDate, fmt.Errorf("From date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
|
||||
}
|
||||
}
|
||||
|
||||
if to != "" {
|
||||
var toErr error
|
||||
toDate, toErr = time.Parse(time.RFC3339, to)
|
||||
if toErr != nil {
|
||||
return fromDate, toDate, fmt.Errorf("To date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
|
||||
}
|
||||
}
|
||||
|
||||
return fromDate, toDate, nil
|
||||
}
|
||||
|
||||
func getSortParameter(r *http.Request) []model.Sort {
|
||||
var sortFields []model.Sort
|
||||
|
||||
queryValues := r.URL.Query()
|
||||
sortString := queryValues.Get("sort")
|
||||
if sortString == "" {
|
||||
return sortFields
|
||||
}
|
||||
|
||||
// Split sort fields up in to slice
|
||||
sorts := strings.Split(sortString, ",")
|
||||
for _, sortItem := range sorts {
|
||||
if strings.Contains(sortItem, ".") {
|
||||
theseItems := strings.Split(sortItem, ".")
|
||||
|
||||
switch strings.ToLower(theseItems[1]) {
|
||||
case "desc":
|
||||
fallthrough
|
||||
case "descending":
|
||||
theseItems[1] = "DESC"
|
||||
default:
|
||||
theseItems[1] = "ASC"
|
||||
}
|
||||
|
||||
sortFields = append(sortFields, model.Sort{
|
||||
Field: theseItems[0],
|
||||
Direction: theseItems[1],
|
||||
})
|
||||
} else {
|
||||
sortFields = append(sortFields, model.Sort{
|
||||
Field: sortItem,
|
||||
Direction: "ASC",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return sortFields
|
||||
}
|
||||
|
||||
func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) {
|
||||
queryValues := r.URL.Query()
|
||||
varValue := queryValues.Get(varName)
|
||||
|
||||
if varValue == "" && required {
|
||||
return 0, fmt.Errorf("%v was not supplied in the request", varName)
|
||||
} else if varValue == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
varInt, intErr := strconv.Atoi(varValue)
|
||||
if intErr != nil {
|
||||
return 0, fmt.Errorf("%v is not a valid number", varName)
|
||||
}
|
||||
|
||||
return varInt, nil
|
||||
}
|
||||
|
||||
func getURLParamInt(r *http.Request, varName string) (int, error) {
|
||||
required := true
|
||||
defaultValue := 0
|
||||
paramStr := chi.URLParam(r, varName)
|
||||
var err error
|
||||
var paramInt int
|
||||
|
||||
if paramStr == "" && required {
|
||||
return 0, fmt.Errorf("%v was not supplied in the request", varName)
|
||||
} else if paramStr == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
if paramInt, err = strconv.Atoi(paramStr); err != nil {
|
||||
return 0, fmt.Errorf("%v is not a valid number", varName)
|
||||
}
|
||||
|
||||
return paramInt, nil
|
||||
}
|
||||
|
||||
func getPagination(r *http.Request) (int, int, error) {
|
||||
var err error
|
||||
offset, err := getQueryVarInt(r, "offset", false, 0)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
limit, err := getQueryVarInt(r, "limit", false, defaultLimit)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return offset, limit, nil
|
||||
}
|
||||
135
backend/internal/api/handler/hosts.go
Normal file
135
backend/internal/api/handler/hosts.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/host"
|
||||
"npm/internal/validator"
|
||||
)
|
||||
|
||||
// GetHosts will return a list of Hosts
|
||||
// Route: GET /hosts
|
||||
func GetHosts() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetHost will return a single Host
|
||||
// Route: GET /hosts/{hostID}
|
||||
func GetHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := host.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHost will create a Host
|
||||
// Route: POST /hosts
|
||||
func CreateHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newHost host.Model
|
||||
err := json.Unmarshal(bodyBytes, &newHost)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newHost.UserID = userID
|
||||
|
||||
if err = validator.ValidateHost(newHost); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newHost.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateHost ...
|
||||
// Route: PUT /hosts/{hostID}
|
||||
func UpdateHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := host.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &host)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = host.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteHost ...
|
||||
// Route: DELETE /hosts/{hostID}
|
||||
func DeleteHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := host.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
14
backend/internal/api/handler/not_allowed.go
Normal file
14
backend/internal/api/handler/not_allowed.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
h "npm/internal/api/http"
|
||||
)
|
||||
|
||||
// NotAllowed is a json error handler for when method is not allowed
|
||||
func NotAllowed() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not allowed", nil)
|
||||
}
|
||||
}
|
||||
65
backend/internal/api/handler/not_found.go
Normal file
65
backend/internal/api/handler/not_found.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
h "npm/internal/api/http"
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var assets embed.FS
|
||||
var assetsSub fs.FS
|
||||
|
||||
var errIsDir = errors.New("path is dir")
|
||||
|
||||
// NotFound is a json error handler for 404's and method not allowed.
|
||||
// It also serves the react frontend as embedded files in the golang binary.
|
||||
func NotFound() func(http.ResponseWriter, *http.Request) {
|
||||
assetsSub, _ = fs.Sub(assets, "assets")
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimLeft(r.URL.Path, "/")
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
err := tryRead(assetsSub, path, w)
|
||||
if err == errIsDir {
|
||||
err = tryRead(assetsSub, "index.html", w)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
} else if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func tryRead(folder fs.FS, requestedPath string, w http.ResponseWriter) error {
|
||||
f, err := folder.Open(requestedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
defer f.Close()
|
||||
|
||||
stat, _ := f.Stat()
|
||||
if stat.IsDir() {
|
||||
return errIsDir
|
||||
}
|
||||
|
||||
contentType := mime.TypeByExtension(filepath.Ext(requestedPath))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
_, err = io.Copy(w, f)
|
||||
return err
|
||||
}
|
||||
99
backend/internal/api/handler/schema.go
Normal file
99
backend/internal/api/handler/schema.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"npm/doc"
|
||||
"npm/internal/api/schema"
|
||||
"npm/internal/config"
|
||||
"npm/internal/logger"
|
||||
|
||||
jsref "github.com/jc21/jsref"
|
||||
"github.com/jc21/jsref/provider"
|
||||
)
|
||||
|
||||
var swaggerSchema []byte
|
||||
|
||||
// Schema simply reads the swagger schema from disk and returns is raw
|
||||
// Route: GET /schema
|
||||
func Schema() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, string(getSchema()))
|
||||
}
|
||||
}
|
||||
|
||||
func getSchema() []byte {
|
||||
if swaggerSchema == nil {
|
||||
// nolint:gosec
|
||||
swaggerSchema, _ = doc.SwaggerFiles.ReadFile("api.swagger.json")
|
||||
|
||||
// Replace {{VERSION}} with Config Version
|
||||
swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version))
|
||||
|
||||
// Dereference the JSON Schema:
|
||||
var schema interface{}
|
||||
if err := json.Unmarshal(swaggerSchema, &schema); err != nil {
|
||||
logger.Error("SwaggerUnmarshalError", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
provider := provider.NewIoFS(doc.SwaggerFiles, "")
|
||||
resolver := jsref.New()
|
||||
err := resolver.AddProvider(provider)
|
||||
if err != nil {
|
||||
logger.Error("SchemaProviderError", err)
|
||||
}
|
||||
|
||||
result, err := resolver.Resolve(schema, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...)
|
||||
if err != nil {
|
||||
logger.Error("SwaggerResolveError", err)
|
||||
} else {
|
||||
var marshalErr error
|
||||
swaggerSchema, marshalErr = json.MarshalIndent(result, "", " ")
|
||||
if marshalErr != nil {
|
||||
logger.Error("SwaggerMarshalError", err)
|
||||
}
|
||||
}
|
||||
// End dereference
|
||||
|
||||
// Replace incoming schemas with those we actually use in code
|
||||
swaggerSchema = replaceIncomingSchemas(swaggerSchema)
|
||||
}
|
||||
return swaggerSchema
|
||||
}
|
||||
|
||||
func replaceIncomingSchemas(swaggerSchema []byte) []byte {
|
||||
str := string(swaggerSchema)
|
||||
|
||||
// Remember to include the double quotes in the replacement!
|
||||
str = strings.ReplaceAll(str, `"{{schema.SetAuth}}"`, schema.SetAuth())
|
||||
str = strings.ReplaceAll(str, `"{{schema.GetToken}}"`, schema.GetToken())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateCertificateAuthority}}"`, schema.CreateCertificateAuthority())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificateAuthority}}"`, schema.UpdateCertificateAuthority())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateCertificate}}"`, schema.CreateCertificate())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificate}}"`, schema.UpdateCertificate(""))
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateSetting}}"`, schema.CreateSetting())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateSetting}}"`, schema.UpdateSetting())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateUser}}"`, schema.CreateUser())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateUser}}"`, schema.UpdateUser())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider())
|
||||
|
||||
return []byte(str)
|
||||
}
|
||||
98
backend/internal/api/handler/settings.go
Normal file
98
backend/internal/api/handler/settings.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/setting"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// GetSettings will return a list of Settings
|
||||
// Route: GET /settings
|
||||
func GetSettings() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := setting.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSetting will return a single Setting
|
||||
// Route: GET /settings/{name}
|
||||
func GetSetting() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
|
||||
sett, err := setting.GetByName(name)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, sett)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSetting will create a Setting
|
||||
// Route: POST /settings
|
||||
func CreateSetting() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newSetting setting.Model
|
||||
err := json.Unmarshal(bodyBytes, &newSetting)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newSetting.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newSetting)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSetting ...
|
||||
// Route: PUT /settings/{name}
|
||||
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
settingName := chi.URLParam(r, "name")
|
||||
|
||||
setting, err := setting.GetByName(settingName)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &setting)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = setting.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, setting)
|
||||
}
|
||||
}
|
||||
}
|
||||
129
backend/internal/api/handler/streams.go
Normal file
129
backend/internal/api/handler/streams.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/stream"
|
||||
)
|
||||
|
||||
// GetStreams will return a list of Streams
|
||||
// Route: GET /hosts/streams
|
||||
func GetStreams() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err := stream.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStream will return a single Streams
|
||||
// Route: GET /hosts/streams/{hostID}
|
||||
func GetStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := stream.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateStream will create a Stream
|
||||
// Route: POST /hosts/steams
|
||||
func CreateStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newHost stream.Model
|
||||
err := json.Unmarshal(bodyBytes, &newHost)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newHost.UserID = userID
|
||||
|
||||
if err = newHost.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Stream: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateStream ...
|
||||
// Route: PUT /hosts/streams/{hostID}
|
||||
func UpdateStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := stream.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &host)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = host.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteStream ...
|
||||
// Route: DELETE /hosts/streams/{hostID}
|
||||
func DeleteStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := stream.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
77
backend/internal/api/handler/tokens.go
Normal file
77
backend/internal/api/handler/tokens.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
h "npm/internal/api/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
"npm/internal/entity/auth"
|
||||
"npm/internal/entity/user"
|
||||
njwt "npm/internal/jwt"
|
||||
)
|
||||
|
||||
// tokenPayload is the structure we expect from a incoming login request
|
||||
type tokenPayload struct {
|
||||
Type string `json:"type"`
|
||||
Identity string `json:"identity"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// NewToken Also known as a Login, requesting a new token with credentials
|
||||
// Route: POST /tokens
|
||||
func NewToken() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Read the bytes from the body
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var payload tokenPayload
|
||||
err := json.Unmarshal(bodyBytes, &payload)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Find user
|
||||
userObj, userErr := user.GetByEmail(payload.Identity)
|
||||
if userErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get Auth
|
||||
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
|
||||
if userErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, authErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify Auth
|
||||
validateErr := authObj.ValidateSecret(payload.Secret)
|
||||
if validateErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, validateErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if response, err := njwt.Generate(&userObj); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshToken an existing token by given them a new one with the same claims
|
||||
// Route: GET /tokens
|
||||
func RefreshToken() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Use your own methods to verify an existing user is
|
||||
// able to refresh their token and then give them a new one
|
||||
userObj, _ := user.GetByEmail("jc@jc21.com")
|
||||
if response, err := njwt.Generate(&userObj); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
206
backend/internal/api/handler/users.go
Normal file
206
backend/internal/api/handler/users.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/auth"
|
||||
"npm/internal/entity/user"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// GetUsers ...
|
||||
// Route: GET /users
|
||||
func GetUsers() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, users)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUser ...
|
||||
// Route: GET /users/{userID}
|
||||
func GetUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID, _, userIDErr := getUserIDFromRequest(r)
|
||||
if userIDErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := user.GetByID(userID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUser ...
|
||||
// Route: PUT /users/{userID}
|
||||
func UpdateUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, self, userIDErr := getUserIDFromRequest(r)
|
||||
if userIDErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := user.GetByID(userID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &user)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsDisabled && self {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot disable yourself!", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = user.Save(); err != nil {
|
||||
if err == errors.ErrDuplicateEmailUser {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser ...
|
||||
// Route: DELETE /users/{userID}
|
||||
func DeleteUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var userID int
|
||||
var err error
|
||||
if userID, err = getURLParamInt(r, "userID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
myUserID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
if myUserID == userID {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot delete yourself!", nil)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := user.GetByID(userID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, user.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser ...
|
||||
// Route: POST /users
|
||||
func CreateUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newUser user.Model
|
||||
err := json.Unmarshal(bodyBytes, &newUser)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newUser.Save(); err != nil {
|
||||
if err == errors.ErrDuplicateEmailUser {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// newUser has been saved, now save their auth
|
||||
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
|
||||
newUser.Auth.UserID = newUser.ID
|
||||
if newUser.Auth.Type == auth.TypePassword {
|
||||
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
|
||||
if err != nil {
|
||||
logger.Error("SetPasswordError", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = newUser.Auth.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
||||
return
|
||||
}
|
||||
|
||||
newUser.Auth.Secret = ""
|
||||
}
|
||||
|
||||
if !config.IsSetup {
|
||||
config.IsSetup = true
|
||||
logger.Info("A new user was created, leaving Setup Mode")
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newUser)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUsers is only available in debug mode for cypress tests
|
||||
// Route: DELETE /users
|
||||
func DeleteUsers() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := user.DeleteAll()
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
// also change setup to true
|
||||
config.IsSetup = false
|
||||
logger.Info("Users have been wiped, entering Setup Mode")
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUserIDFromRequest(r *http.Request) (int, bool, error) {
|
||||
userIDstr := chi.URLParam(r, "userID")
|
||||
|
||||
var userID int
|
||||
self := false
|
||||
if userIDstr == "me" {
|
||||
// Get user id from Token
|
||||
userID, _ = r.Context().Value(c.UserIDCtxKey).(int)
|
||||
self = true
|
||||
} else {
|
||||
var userIDerr error
|
||||
if userID, userIDerr = getURLParamInt(r, "userID"); userIDerr != nil {
|
||||
return 0, false, userIDerr
|
||||
}
|
||||
}
|
||||
return userID, self, nil
|
||||
}
|
||||
46
backend/internal/api/http/requests.go
Normal file
46
backend/internal/api/http/requests.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidJSON ...
|
||||
ErrInvalidJSON = errors.New("JSON is invalid")
|
||||
// ErrInvalidPayload ...
|
||||
ErrInvalidPayload = errors.New("Payload is invalid")
|
||||
)
|
||||
|
||||
// ValidateRequestSchema takes a Schema and the Content to validate against it
|
||||
func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) {
|
||||
var jsonErrors []jsonschema.KeyError
|
||||
var schemaBytes = []byte(schema)
|
||||
|
||||
// Make sure the body is valid JSON
|
||||
if !isJSON(requestBody) {
|
||||
return jsonErrors, ErrInvalidJSON
|
||||
}
|
||||
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal(schemaBytes, rs); err != nil {
|
||||
return jsonErrors, err
|
||||
}
|
||||
|
||||
var validationErr error
|
||||
ctx := context.TODO()
|
||||
if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 {
|
||||
return jsonErrors, validationErr
|
||||
}
|
||||
|
||||
// Valid
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func isJSON(bytes []byte) bool {
|
||||
var js map[string]interface{}
|
||||
return json.Unmarshal(bytes, &js) == nil
|
||||
}
|
||||
90
backend/internal/api/http/responses.go
Normal file
90
backend/internal/api/http/responses.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// Response interface for standard API results
|
||||
type Response struct {
|
||||
Result interface{} `json:"result"`
|
||||
Error interface{} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse interface for errors returned via the API
|
||||
type ErrorResponse struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message interface{} `json:"message"`
|
||||
Invalid interface{} `json:"invalid,omitempty"`
|
||||
}
|
||||
|
||||
// ResultResponseJSON will write the result as json to the http output
|
||||
func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
|
||||
var response Response
|
||||
resultClass := fmt.Sprintf("%v", reflect.TypeOf(result))
|
||||
|
||||
if resultClass == "http.ErrorResponse" {
|
||||
response = Response{
|
||||
Error: result,
|
||||
}
|
||||
} else {
|
||||
response = Response{
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
var err error
|
||||
if getPrettyPrintFromContext(r) {
|
||||
payload, err = json.MarshalIndent(response, "", " ")
|
||||
} else {
|
||||
payload, err = json.Marshal(response)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("ResponseMarshalError", err)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, string(payload))
|
||||
}
|
||||
|
||||
// ResultSchemaErrorJSON will format the result as a standard error object and send it for output
|
||||
func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errors []jsonschema.KeyError) {
|
||||
errorResponse := ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: "Request failed validation",
|
||||
Invalid: errors,
|
||||
}
|
||||
|
||||
ResultResponseJSON(w, r, http.StatusBadRequest, errorResponse)
|
||||
}
|
||||
|
||||
// ResultErrorJSON will format the result as a standard error object and send it for output
|
||||
func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) {
|
||||
errorResponse := ErrorResponse{
|
||||
Code: status,
|
||||
Message: message,
|
||||
Invalid: extended,
|
||||
}
|
||||
|
||||
ResultResponseJSON(w, r, status, errorResponse)
|
||||
}
|
||||
|
||||
// getPrettyPrintFromContext returns the PrettyPrint setting
|
||||
func getPrettyPrintFromContext(r *http.Request) bool {
|
||||
pretty, ok := r.Context().Value(c.PrettyPrintCtxKey).(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return pretty
|
||||
}
|
||||
13
backend/internal/api/middleware/access_control.go
Normal file
13
backend/internal/api/middleware/access_control.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AccessControl sets http headers for responses
|
||||
func AccessControl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
64
backend/internal/api/middleware/auth.go
Normal file
64
backend/internal/api/middleware/auth.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/user"
|
||||
njwt "npm/internal/jwt"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/go-chi/jwtauth"
|
||||
)
|
||||
|
||||
// DecodeAuth ...
|
||||
func DecodeAuth() func(http.Handler) http.Handler {
|
||||
privateKey, privateKeyParseErr := njwt.GetPrivateKey()
|
||||
if privateKeyParseErr != nil && privateKey == nil {
|
||||
logger.Error("PrivateKeyParseError", privateKeyParseErr)
|
||||
}
|
||||
|
||||
publicKey, publicKeyParseErr := njwt.GetPublicKey()
|
||||
if publicKeyParseErr != nil && publicKey == nil {
|
||||
logger.Error("PublicKeyParseError", publicKeyParseErr)
|
||||
}
|
||||
|
||||
tokenAuth := jwtauth.New("RS256", privateKey, publicKey)
|
||||
return jwtauth.Verifier(tokenAuth)
|
||||
}
|
||||
|
||||
// Enforce is a authentication middleware to enforce access from the
|
||||
// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised
|
||||
// response for any unverified tokens and passes the good ones through.
|
||||
func Enforce() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if config.IsSetup {
|
||||
token, claims, err := jwtauth.FromContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
userID := int(claims["uid"].(float64))
|
||||
_, enabled := user.IsEnabled(userID)
|
||||
if token == nil || !token.Valid || !enabled {
|
||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Add claims to context
|
||||
ctx = context.WithValue(ctx, c.UserIDCtxKey, userID)
|
||||
}
|
||||
|
||||
// Token is authenticated, continue as normal
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
26
backend/internal/api/middleware/body_context.go
Normal file
26
backend/internal/api/middleware/body_context.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// BodyContext simply adds the body data to a context item
|
||||
func BodyContext() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Grab the Body Data
|
||||
var body []byte
|
||||
if r.Body != nil {
|
||||
body, _ = ioutil.ReadAll(r.Body)
|
||||
}
|
||||
// Add it to the context
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.BodyCtxKey, body)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
88
backend/internal/api/middleware/cors.go
Normal file
88
backend/internal/api/middleware/cors.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
var methodMap = []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodConnect,
|
||||
http.MethodTrace,
|
||||
}
|
||||
|
||||
func getRouteMethods(routes chi.Router, path string) []string {
|
||||
var methods []string
|
||||
tctx := chi.NewRouteContext()
|
||||
for _, method := range methodMap {
|
||||
if routes.Match(tctx, method, path) {
|
||||
methods = append(methods, method)
|
||||
}
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
var headersAllowedByCORS = []string{
|
||||
"Authorization",
|
||||
"Host",
|
||||
"Content-Type",
|
||||
"Connection",
|
||||
"User-Agent",
|
||||
"Cache-Control",
|
||||
"Accept-Encoding",
|
||||
"X-Jumbo-AppKey",
|
||||
"X-Jumbo-SKey",
|
||||
"X-Jumbo-SV",
|
||||
"X-Jumbo-Timestamp",
|
||||
"X-Jumbo-Version",
|
||||
"X-Jumbo-Customer-Id",
|
||||
}
|
||||
|
||||
// Cors ...
|
||||
func Cors(routes chi.Router) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
methods := getRouteMethods(routes, r.URL.Path)
|
||||
if len(methods) == 0 {
|
||||
// no route no cors
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
methods = append(methods, http.MethodOptions)
|
||||
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
|
||||
w.Header().Set("Access-Control-Allow-Headers",
|
||||
strings.Join(headersAllowedByCORS, ","),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Options ...
|
||||
func Options(routes chi.Router) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
methods := getRouteMethods(routes, r.URL.Path)
|
||||
if len(methods) == 0 {
|
||||
// no route shouldn't have options
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, "{}")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
28
backend/internal/api/middleware/enforce_setup.go
Normal file
28
backend/internal/api/middleware/enforce_setup.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
// EnforceSetup will error if the config setup doesn't match what is required
|
||||
func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.IsSetup != shouldBeSetup {
|
||||
state := "during"
|
||||
if config.IsSetup {
|
||||
state = "after"
|
||||
}
|
||||
h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// All good
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
114
backend/internal/api/middleware/filters.go
Normal file
114
backend/internal/api/middleware/filters.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/model"
|
||||
"npm/internal/util"
|
||||
"strings"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// Filters will accept a pre-defined schemaData to validate against the GET query params
|
||||
// passed in to this endpoint. This will ensure that the filters are not injecting SQL.
|
||||
// After we have determined what the Filters are to be, they are saved on the Context
|
||||
// to be used later in other endpoints.
|
||||
func Filters(schemaData string) func(http.Handler) http.Handler {
|
||||
reservedFilterKeys := []string{
|
||||
"limit",
|
||||
"offset",
|
||||
"sort",
|
||||
"order",
|
||||
"t", // This is used as a timestamp paramater in some clients and can be ignored
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var filters []model.Filter
|
||||
for key, val := range r.URL.Query() {
|
||||
key = strings.ToLower(key)
|
||||
|
||||
// Split out the modifier from the field name and set a default modifier
|
||||
var keyParts []string
|
||||
keyParts = strings.Split(key, ":")
|
||||
if len(keyParts) == 1 {
|
||||
// Default modifier
|
||||
keyParts = append(keyParts, "equals")
|
||||
}
|
||||
|
||||
// Only use this filter if it's not a reserved get param
|
||||
if !util.SliceContainsItem(reservedFilterKeys, keyParts[0]) {
|
||||
for _, valItem := range val {
|
||||
// Check that the val isn't empty
|
||||
if len(strings.TrimSpace(valItem)) > 0 {
|
||||
valSlice := []string{valItem}
|
||||
if keyParts[1] == "in" || keyParts[1] == "notin" {
|
||||
valSlice = strings.Split(valItem, ",")
|
||||
}
|
||||
|
||||
filters = append(filters, model.Filter{
|
||||
Field: keyParts[0],
|
||||
Modifier: keyParts[1],
|
||||
Value: valSlice,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only validate schema if there are filters to validate
|
||||
if len(filters) > 0 {
|
||||
ctx := r.Context()
|
||||
|
||||
// Marshal the Filters in to a JSON string so that the Schema Validation works against it
|
||||
filterData, marshalErr := json.MarshalIndent(filters, "", " ")
|
||||
if marshalErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create root schema
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate it
|
||||
errors, jsonError := rs.ValidateBytes(ctx, filterData)
|
||||
if jsonError != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, jsonError.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Invalid Filters", errors)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, c.FiltersCtxKey, filters)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetFiltersFromContext returns the Filters
|
||||
func GetFiltersFromContext(r *http.Request) []model.Filter {
|
||||
filters, ok := r.Context().Value(c.FiltersCtxKey).([]model.Filter)
|
||||
if !ok {
|
||||
// the assertion failed
|
||||
var emptyFilters []model.Filter
|
||||
return emptyFilters
|
||||
}
|
||||
return filters
|
||||
}
|
||||
23
backend/internal/api/middleware/pretty_print.go
Normal file
23
backend/internal/api/middleware/pretty_print.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// PrettyPrint will determine whether the request should be pretty printed in output
|
||||
// with ?pretty=1 or ?pretty=true
|
||||
func PrettyPrint(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
prettyStr := r.URL.Query().Get("pretty")
|
||||
if prettyStr == "1" || prettyStr == "true" {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.PrettyPrintCtxKey, true)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
55
backend/internal/api/middleware/schema.go
Normal file
55
backend/internal/api/middleware/schema.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// CheckRequestSchema ...
|
||||
func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte) ([]jsonschema.KeyError, error) {
|
||||
// Create root schema
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
|
||||
return nil, fmt.Errorf("Schema Fatal: %v", err)
|
||||
}
|
||||
|
||||
// Validate it
|
||||
schemaErrors, jsonError := rs.ValidateBytes(ctx, payload)
|
||||
if jsonError != nil {
|
||||
return nil, jsonError
|
||||
}
|
||||
|
||||
return schemaErrors, nil
|
||||
}
|
||||
|
||||
// EnforceRequestSchema accepts a schema and validates the request body against it
|
||||
func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get content from context
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
schemaErrors, err := CheckRequestSchema(r.Context(), schemaData, bodyBytes)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(schemaErrors) > 0 {
|
||||
h.ResultSchemaErrorJSON(w, r, schemaErrors)
|
||||
return
|
||||
}
|
||||
|
||||
// All good
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
171
backend/internal/api/router.go
Normal file
171
backend/internal/api/router.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"npm/internal/api/handler"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/api/schema"
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/certificate"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
"npm/internal/entity/host"
|
||||
"npm/internal/entity/setting"
|
||||
"npm/internal/entity/stream"
|
||||
"npm/internal/entity/user"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
chiMiddleware "github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
)
|
||||
|
||||
// NewRouter returns a new router object
|
||||
func NewRouter() http.Handler {
|
||||
// Cors
|
||||
cors := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(
|
||||
middleware.AccessControl,
|
||||
middleware.Cors(r),
|
||||
middleware.Options(r),
|
||||
cors.Handler,
|
||||
chiMiddleware.RealIP,
|
||||
chiMiddleware.Recoverer,
|
||||
chiMiddleware.Throttle(5),
|
||||
chiMiddleware.Timeout(30*time.Second),
|
||||
middleware.PrettyPrint,
|
||||
middleware.DecodeAuth(),
|
||||
middleware.BodyContext(),
|
||||
)
|
||||
|
||||
return applyRoutes(r)
|
||||
}
|
||||
|
||||
// applyRoutes is where the magic happens
|
||||
func applyRoutes(r chi.Router) chi.Router {
|
||||
r.NotFound(handler.NotFound())
|
||||
r.MethodNotAllowed(handler.NotAllowed())
|
||||
|
||||
// API
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Get("/", handler.Health())
|
||||
r.Get("/schema", handler.Schema())
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce()).
|
||||
Get("/config", handler.Config())
|
||||
|
||||
// Tokens
|
||||
r.With(middleware.EnforceSetup(true)).Route("/tokens", func(r chi.Router) {
|
||||
r.With(middleware.EnforceRequestSchema(schema.GetToken())).
|
||||
Post("/", handler.NewToken())
|
||||
r.With(middleware.Enforce()).
|
||||
Get("/", handler.RefreshToken())
|
||||
})
|
||||
|
||||
// Users
|
||||
r.With(middleware.Enforce()).Route("/users", func(r chi.Router) {
|
||||
r.With(middleware.EnforceSetup(true)).Get("/{userID:(?:[0-9]+|me)}", handler.GetUser())
|
||||
r.With(middleware.EnforceSetup(true)).Delete("/{userID:(?:[0-9]+|me)}", handler.DeleteUser())
|
||||
r.With(middleware.EnforceSetup(true)).With(middleware.Filters(user.GetFilterSchema())).
|
||||
Get("/", handler.GetUsers())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateUser())).
|
||||
Post("/", handler.CreateUser())
|
||||
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser())).
|
||||
Put("/{userID:(?:[0-9]+|me)}", handler.UpdateUser())
|
||||
|
||||
// Auth
|
||||
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth())).
|
||||
Post("/{userID:(?:[0-9]+|me)}/auth", handler.SetAuth())
|
||||
})
|
||||
|
||||
// Only available in debug mode: delete users without auth
|
||||
if config.GetLogLevel() == logger.DebugLevel {
|
||||
r.Delete("/users", handler.DeleteUsers())
|
||||
}
|
||||
|
||||
// Settings
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/settings", func(r chi.Router) {
|
||||
r.With(middleware.Filters(setting.GetFilterSchema())).
|
||||
Get("/", handler.GetSettings())
|
||||
r.Get("/{name}", handler.GetSetting())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateSetting())).
|
||||
Post("/", handler.CreateSetting())
|
||||
r.With(middleware.EnforceRequestSchema(schema.UpdateSetting())).
|
||||
Put("/{name}", handler.UpdateSetting())
|
||||
})
|
||||
|
||||
// DNS Providers
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/dns-providers", func(r chi.Router) {
|
||||
r.With(middleware.Filters(dnsprovider.GetFilterSchema())).
|
||||
Get("/", handler.GetDNSProviders())
|
||||
r.Get("/{providerID:[0-9]+}", handler.GetDNSProvider())
|
||||
r.Delete("/{providerID:[0-9]+}", handler.DeleteDNSProvider())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateDNSProvider())).
|
||||
Post("/", handler.CreateDNSProvider())
|
||||
r.With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())).
|
||||
Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider())
|
||||
})
|
||||
|
||||
// Certificate Authorities
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/certificate-authorities", func(r chi.Router) {
|
||||
r.With(middleware.Filters(certificateauthority.GetFilterSchema())).
|
||||
Get("/", handler.GetCertificateAuthorities())
|
||||
r.Get("/{caID:[0-9]+}", handler.GetCertificateAuthority())
|
||||
r.Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())).
|
||||
Post("/", handler.CreateCertificateAuthority())
|
||||
r.With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())).
|
||||
Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority())
|
||||
})
|
||||
|
||||
// Certificates
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/certificates", func(r chi.Router) {
|
||||
r.With(middleware.Filters(certificate.GetFilterSchema())).
|
||||
Get("/", handler.GetCertificates())
|
||||
r.Get("/{certificateID:[0-9]+}", handler.GetCertificate())
|
||||
r.Delete("/{certificateID:[0-9]+}", handler.DeleteCertificate())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateCertificate())).
|
||||
Post("/", handler.CreateCertificate())
|
||||
/*
|
||||
r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())).
|
||||
Put("/{certificateID:[0-9]+}", handler.UpdateCertificate())
|
||||
*/
|
||||
r.Put("/{certificateID:[0-9]+}", handler.UpdateCertificate())
|
||||
})
|
||||
|
||||
// Hosts
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/hosts", func(r chi.Router) {
|
||||
r.With(middleware.Filters(host.GetFilterSchema())).
|
||||
Get("/", handler.GetHosts())
|
||||
r.Get("/{hostID:[0-9]+}", handler.GetHost())
|
||||
r.Delete("/{hostID:[0-9]+}", handler.DeleteHost())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateHost())).
|
||||
Post("/", handler.CreateHost())
|
||||
r.With(middleware.EnforceRequestSchema(schema.UpdateHost())).
|
||||
Put("/{hostID:[0-9]+}", handler.UpdateHost())
|
||||
})
|
||||
|
||||
// Streams
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/streams", func(r chi.Router) {
|
||||
r.With(middleware.Filters(stream.GetFilterSchema())).
|
||||
Get("/", handler.GetStreams())
|
||||
r.Get("/{hostID:[0-9]+}", handler.GetStream())
|
||||
r.Delete("/{hostID:[0-9]+}", handler.DeleteStream())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateStream())).
|
||||
Post("/", handler.CreateStream())
|
||||
r.With(middleware.EnforceRequestSchema(schema.UpdateStream())).
|
||||
Put("/{hostID:[0-9]+}", handler.UpdateStream())
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
44
backend/internal/api/router_test.go
Normal file
44
backend/internal/api/router_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"npm/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
r = NewRouter()
|
||||
version = "3.0.0"
|
||||
commit = "abcdefgh"
|
||||
sentryDSN = ""
|
||||
)
|
||||
|
||||
// Tear up/down
|
||||
func TestMain(m *testing.M) {
|
||||
config.Init(&version, &commit, &sentryDSN)
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestGetHealthz(t *testing.T) {
|
||||
respRec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/", nil)
|
||||
|
||||
r.ServeHTTP(respRec, req)
|
||||
assert.Equal(t, http.StatusOK, respRec.Code)
|
||||
assert.Contains(t, respRec.Body.String(), "healthy")
|
||||
}
|
||||
|
||||
func TestNonExistent(t *testing.T) {
|
||||
respRec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/non-existent-endpoint", nil)
|
||||
|
||||
r.ServeHTTP(respRec, req)
|
||||
assert.Equal(t, http.StatusNotFound, respRec.Code)
|
||||
assert.Equal(t, respRec.Body.String(), `{"result":null,"error":{"code":404,"message":"Not found"}}`, "404 Message should match")
|
||||
}
|
||||
191
backend/internal/api/schema/certificates.go
Normal file
191
backend/internal/api/schema/certificates.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"npm/internal/entity/certificate"
|
||||
)
|
||||
|
||||
// This validation is strictly for Custom certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateCustom() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, strictString("custom"), stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// This validation is strictly for HTTP certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateHTTP() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"certificate_authority_id",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"certificate_authority_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, strictString("http"), intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// This validation is strictly for DNS certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateDNS() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"certificate_authority_id",
|
||||
"dns_provider_id",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"certificate_authority_id": %s,
|
||||
"dns_provider_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, strictString("dns"), intMinOne, intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// This validation is strictly for MKCERT certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateMkcert() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, strictString("mkcert"), stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateHTTP() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"certificate_authority_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateDNS() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"certificate_authority_id": %s,
|
||||
"dns_provider_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, intMinOne, intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateCustom() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateMkcert() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// CreateCertificate is the schema for incoming data validation
|
||||
func CreateCertificate() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"oneOf": [%s, %s, %s, %s]
|
||||
}`, createCertificateHTTP(), createCertificateDNS(), createCertificateCustom(), createCertificateMkcert())
|
||||
}
|
||||
|
||||
// UpdateCertificate is the schema for incoming data validation
|
||||
func UpdateCertificate(certificateType string) string {
|
||||
switch certificateType {
|
||||
case certificate.TypeHTTP:
|
||||
return updateCertificateHTTP()
|
||||
case certificate.TypeDNS:
|
||||
return updateCertificateDNS()
|
||||
case certificate.TypeCustom:
|
||||
return updateCertificateCustom()
|
||||
case certificate.TypeMkcert:
|
||||
return updateCertificateMkcert()
|
||||
default:
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"oneOf": [%s, %s, %s, %s]
|
||||
}`, updateCertificateHTTP(), updateCertificateDNS(), updateCertificateCustom(), updateCertificateMkcert())
|
||||
}
|
||||
}
|
||||
61
backend/internal/api/schema/common.go
Normal file
61
backend/internal/api/schema/common.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
func strictString(value string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
}`, value)
|
||||
}
|
||||
|
||||
const intMinOne = `
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
`
|
||||
|
||||
func stringMinMax(minLength, maxLength int) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "string",
|
||||
"minLength": %d,
|
||||
"maxLength": %d
|
||||
}`, minLength, maxLength)
|
||||
}
|
||||
|
||||
func userRoles() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "array",
|
||||
"items": %s
|
||||
}`, stringMinMax(2, 50))
|
||||
}
|
||||
|
||||
func domainNames() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": %s
|
||||
}`, stringMinMax(4, 255))
|
||||
}
|
||||
|
||||
const anyType = `
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
21
backend/internal/api/schema/create_certificate_authority.go
Normal file
21
backend/internal/api/schema/create_certificate_authority.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateCertificateAuthority is the schema for incoming data validation
|
||||
func CreateCertificateAuthority() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"acme2_url"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"acme2_url": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100), stringMinMax(8, 255))
|
||||
}
|
||||
25
backend/internal/api/schema/create_dns_provider.go
Normal file
25
backend/internal/api/schema/create_dns_provider.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateDNSProvider is the schema for incoming data validation
|
||||
func CreateDNSProvider() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"provider_key",
|
||||
"name",
|
||||
"meta"
|
||||
],
|
||||
"properties": {
|
||||
"provider_key": %s,
|
||||
"name": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(1, 100))
|
||||
}
|
||||
75
backend/internal/api/schema/create_host.go
Normal file
75
backend/internal/api/schema/create_host.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateHost is the schema for incoming data validation
|
||||
// This schema supports 3 possible types with different data combinations:
|
||||
// - proxy
|
||||
// - redirection
|
||||
// - dead
|
||||
func CreateHost() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^proxy$"
|
||||
},
|
||||
"listen_interface": %s,
|
||||
"domain_names": %s,
|
||||
"upstream_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"certificate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"access_list_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ssl_forced": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"caching_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"block_exploits": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allow_websocket_upgrade": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"http2_support": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hsts_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hsts_subdomains": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"paths": {
|
||||
"type": "string"
|
||||
},
|
||||
"upstream_options": {
|
||||
"type": "string"
|
||||
},
|
||||
"advanced_config": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_disabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`, stringMinMax(0, 255), domainNames())
|
||||
}
|
||||
21
backend/internal/api/schema/create_setting.go
Normal file
21
backend/internal/api/schema/create_setting.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateSetting is the schema for incoming data validation
|
||||
func CreateSetting() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"value": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), anyType)
|
||||
}
|
||||
27
backend/internal/api/schema/create_stream.go
Normal file
27
backend/internal/api/schema/create_stream.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateStream is the schema for incoming data validation
|
||||
func CreateStream() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"provider",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"provider": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"expires_on": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
|
||||
}
|
||||
42
backend/internal/api/schema/create_user.go
Normal file
42
backend/internal/api/schema/create_user.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateUser is the schema for incoming data validation
|
||||
func CreateUser() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"roles",
|
||||
"is_disabled"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"nickname": %s,
|
||||
"email": %s,
|
||||
"roles": %s,
|
||||
"is_disabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^password$"
|
||||
},
|
||||
"secret": %s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), userRoles(), stringMinMax(8, 255))
|
||||
}
|
||||
28
backend/internal/api/schema/get_token.go
Normal file
28
backend/internal/api/schema/get_token.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GetToken is the schema for incoming data validation
|
||||
// nolint: gosec
|
||||
func GetToken() string {
|
||||
stdField := stringMinMax(1, 255)
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"identity",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^password$"
|
||||
},
|
||||
"identity": %s,
|
||||
"secret": %s
|
||||
}
|
||||
}
|
||||
`, stdField, stdField)
|
||||
}
|
||||
21
backend/internal/api/schema/set_auth.go
Normal file
21
backend/internal/api/schema/set_auth.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SetAuth is the schema for incoming data validation
|
||||
func SetAuth() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"value": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), anyType)
|
||||
}
|
||||
17
backend/internal/api/schema/update_certificate_authority.go
Normal file
17
backend/internal/api/schema/update_certificate_authority.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateCertificateAuthority is the schema for incoming data validation
|
||||
func UpdateCertificateAuthority() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"acme2_url": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100), stringMinMax(8, 255))
|
||||
}
|
||||
19
backend/internal/api/schema/update_dns_provider.go
Normal file
19
backend/internal/api/schema/update_dns_provider.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateDNSProvider is the schema for incoming data validation
|
||||
func UpdateDNSProvider() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100))
|
||||
}
|
||||
22
backend/internal/api/schema/update_host.go
Normal file
22
backend/internal/api/schema/update_host.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateHost is the schema for incoming data validation
|
||||
func UpdateHost() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"expires_on": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
|
||||
}
|
||||
16
backend/internal/api/schema/update_setting.go
Normal file
16
backend/internal/api/schema/update_setting.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateSetting is the schema for incoming data validation
|
||||
func UpdateSetting() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": %s
|
||||
}
|
||||
}
|
||||
`, anyType)
|
||||
}
|
||||
22
backend/internal/api/schema/update_stream.go
Normal file
22
backend/internal/api/schema/update_stream.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateStream is the schema for incoming data validation
|
||||
func UpdateStream() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"expires_on": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
|
||||
}
|
||||
22
backend/internal/api/schema/update_user.go
Normal file
22
backend/internal/api/schema/update_user.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateUser is the schema for incoming data validation
|
||||
func UpdateUser() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"nickname": %s,
|
||||
"email": %s,
|
||||
"roles": %s,
|
||||
"is_disabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), userRoles())
|
||||
}
|
||||
19
backend/internal/api/server.go
Normal file
19
backend/internal/api/server.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
const httpPort = 3000
|
||||
|
||||
// StartServer creates a http server
|
||||
func StartServer() {
|
||||
logger.Info("Server starting on port %v", httpPort)
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%v", httpPort), NewRouter())
|
||||
if err != nil {
|
||||
logger.Error("HttpListenError", err)
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
const error = require('../lib/error');
|
||||
const auditLogModel = require('../models/audit-log');
|
||||
|
||||
const internalAuditLog = {
|
||||
|
||||
/**
|
||||
* All logs
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {Array} [expand]
|
||||
* @param {String} [search_query]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAll: (access, expand, search_query) => {
|
||||
return access.can('auditlog:list')
|
||||
.then(() => {
|
||||
let query = auditLogModel
|
||||
.query()
|
||||
.orderBy('created_on', 'DESC')
|
||||
.orderBy('id', 'DESC')
|
||||
.limit(100)
|
||||
.allowEager('[user]');
|
||||
|
||||
// Query is used for searching
|
||||
if (typeof search_query === 'string') {
|
||||
query.where(function () {
|
||||
this.where('meta', 'like', '%' + search_query + '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof expand !== 'undefined' && expand !== null) {
|
||||
query.eager('[' + expand.join(', ') + ']');
|
||||
}
|
||||
|
||||
return query;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This method should not be publicly used, it doesn't check certain things. It will be assumed
|
||||
* that permission to add to audit log is already considered, however the access token is used for
|
||||
* default user id determination.
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {String} data.action
|
||||
* @param {Number} [data.user_id]
|
||||
* @param {Number} [data.object_id]
|
||||
* @param {Number} [data.object_type]
|
||||
* @param {Object} [data.meta]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add: (access, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 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) {
|
||||
reject(new error.InternalValidationError('Audit log entry must contain an Action'));
|
||||
} else {
|
||||
// Make sure at least 1 of the IDs are set and action
|
||||
resolve(auditLogModel
|
||||
.query()
|
||||
.insert({
|
||||
user_id: data.user_id,
|
||||
action: data.action,
|
||||
object_type: data.object_type || '',
|
||||
object_id: data.object_id || 0,
|
||||
meta: data.meta || {}
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = internalAuditLog;
|
||||
51
backend/internal/cache/cache.go
vendored
Normal file
51
backend/internal/cache/cache.go
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"npm/internal/entity/setting"
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
// Cache is a memory cache
|
||||
type Cache struct {
|
||||
Settings *map[string]setting.Model
|
||||
}
|
||||
|
||||
// Status is the status of last update
|
||||
type Status struct {
|
||||
LastUpdate time.Time
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// NewCache will create and return a new Cache object
|
||||
func NewCache() *Cache {
|
||||
return &Cache{
|
||||
Settings: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh will refresh all cache items
|
||||
func (c *Cache) Refresh() {
|
||||
c.RefreshSettings()
|
||||
}
|
||||
|
||||
// Clear will clear the cache
|
||||
func (c *Cache) Clear() {
|
||||
c.Settings = nil
|
||||
}
|
||||
|
||||
// RefreshSettings will refresh the settings from db
|
||||
func (c *Cache) RefreshSettings() {
|
||||
logger.Info("Cache refreshing Settings")
|
||||
/*
|
||||
c.ProductOffers = client.GetProductOffers()
|
||||
|
||||
if c.ProductOffers != nil {
|
||||
c.Status["product_offers"] = Status{
|
||||
LastUpdate: time.Now(),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
78
backend/internal/config/config.go
Normal file
78
backend/internal/config/config.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
golog "log"
|
||||
"runtime"
|
||||
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/vrischmann/envconfig"
|
||||
)
|
||||
|
||||
// Init will parse environment variables into the Env struct
|
||||
func Init(version, commit, sentryDSN *string) {
|
||||
// ErrorReporting is enabled until we load the status of it from the DB later
|
||||
ErrorReporting = true
|
||||
|
||||
Version = *version
|
||||
Commit = *commit
|
||||
|
||||
if err := envconfig.Init(&Configuration); err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
}
|
||||
|
||||
initLogger(*sentryDSN)
|
||||
logger.Info("Build Version: %s (%s)", Version, Commit)
|
||||
loadKeys()
|
||||
}
|
||||
|
||||
// Init initialises the Log object and return it
|
||||
func initLogger(sentryDSN string) {
|
||||
// this removes timestamp prefixes from logs
|
||||
golog.SetFlags(0)
|
||||
|
||||
switch Configuration.Log.Level {
|
||||
case "debug":
|
||||
logLevel = logger.DebugLevel
|
||||
case "warn":
|
||||
logLevel = logger.WarnLevel
|
||||
case "error":
|
||||
logLevel = logger.ErrorLevel
|
||||
default:
|
||||
logLevel = logger.InfoLevel
|
||||
}
|
||||
|
||||
err := logger.Configure(&logger.Config{
|
||||
LogThreshold: logLevel,
|
||||
Formatter: Configuration.Log.Format,
|
||||
SentryConfig: sentry.ClientOptions{
|
||||
// This is the jc21 NginxProxyManager Sentry project,
|
||||
// errors will be reported here (if error reporting is enable)
|
||||
// and this project is private. No personal information should
|
||||
// be sent in any error messages, only stacktraces.
|
||||
Dsn: sentryDSN,
|
||||
Release: Commit,
|
||||
Dist: Version,
|
||||
Environment: fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Error("LoggerConfigurationError", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogLevel returns the logger const level
|
||||
func GetLogLevel() logger.Level {
|
||||
return logLevel
|
||||
}
|
||||
|
||||
func isError(errorClass string, err error) bool {
|
||||
if err != nil {
|
||||
logger.Error(errorClass, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
112
backend/internal/config/keys.go
Normal file
112
backend/internal/config/keys.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
var keysFolder string
|
||||
var publicKeyFile string
|
||||
var privateKeyFile string
|
||||
|
||||
func loadKeys() {
|
||||
// check if keys folder exists in data folder
|
||||
keysFolder = fmt.Sprintf("%s/keys", Configuration.DataFolder)
|
||||
publicKeyFile = fmt.Sprintf("%s/public.key", keysFolder)
|
||||
privateKeyFile = fmt.Sprintf("%s/private.key", keysFolder)
|
||||
|
||||
if _, err := os.Stat(keysFolder); os.IsNotExist(err) {
|
||||
// nolint:errcheck,gosec
|
||||
os.Mkdir(keysFolder, 0700)
|
||||
}
|
||||
|
||||
// check if keys exist on disk
|
||||
_, publicKeyErr := os.Stat(publicKeyFile)
|
||||
_, privateKeyErr := os.Stat(privateKeyFile)
|
||||
|
||||
// generate keys if either one doesn't exist
|
||||
if os.IsNotExist(publicKeyErr) || os.IsNotExist(privateKeyErr) {
|
||||
generateKeys()
|
||||
saveKeys()
|
||||
}
|
||||
|
||||
// Load keys from disk
|
||||
// nolint:gosec
|
||||
publicKeyBytes, publicKeyBytesErr := ioutil.ReadFile(publicKeyFile)
|
||||
// nolint:gosec
|
||||
privateKeyBytes, privateKeyBytesErr := ioutil.ReadFile(privateKeyFile)
|
||||
PublicKey = string(publicKeyBytes)
|
||||
PrivateKey = string(privateKeyBytes)
|
||||
|
||||
if isError("PublicKeyReadError", publicKeyBytesErr) || isError("PrivateKeyReadError", privateKeyBytesErr) || PublicKey == "" || PrivateKey == "" {
|
||||
logger.Warn("There was an error loading keys, proceeding to generate new RSA keys")
|
||||
generateKeys()
|
||||
saveKeys()
|
||||
}
|
||||
}
|
||||
|
||||
func generateKeys() {
|
||||
reader := rand.Reader
|
||||
bitSize := 4096
|
||||
|
||||
key, err := rsa.GenerateKey(reader, bitSize)
|
||||
if isError("RSAGenerateError", err) {
|
||||
return
|
||||
}
|
||||
|
||||
privateKey := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}
|
||||
|
||||
privateKeyBuffer := new(bytes.Buffer)
|
||||
err = pem.Encode(privateKeyBuffer, privateKey)
|
||||
if isError("PrivatePEMEncodeError", err) {
|
||||
return
|
||||
}
|
||||
|
||||
asn1Bytes, err2 := asn1.Marshal(key.PublicKey)
|
||||
if isError("RSAMarshalError", err2) {
|
||||
return
|
||||
}
|
||||
|
||||
publicKey := &pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: asn1Bytes,
|
||||
}
|
||||
|
||||
publicKeyBuffer := new(bytes.Buffer)
|
||||
err = pem.Encode(publicKeyBuffer, publicKey)
|
||||
if isError("PublicPEMEncodeError", err) {
|
||||
return
|
||||
}
|
||||
|
||||
PublicKey = publicKeyBuffer.String()
|
||||
PrivateKey = privateKeyBuffer.String()
|
||||
logger.Info("Generated new RSA keys")
|
||||
}
|
||||
|
||||
func saveKeys() {
|
||||
err := ioutil.WriteFile(publicKeyFile, []byte(PublicKey), 0600)
|
||||
if err != nil {
|
||||
logger.Error("PublicKeyWriteError", err)
|
||||
} else {
|
||||
logger.Info("Saved Public Key: %s", publicKeyFile)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(privateKeyFile, []byte(PrivateKey), 0600)
|
||||
if err != nil {
|
||||
logger.Error("PrivateKeyWriteError", err)
|
||||
} else {
|
||||
logger.Info("Saved Private Key: %s", privateKeyFile)
|
||||
}
|
||||
}
|
||||
34
backend/internal/config/vars.go
Normal file
34
backend/internal/config/vars.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import "npm/internal/logger"
|
||||
|
||||
// Version is the version set by ldflags
|
||||
var Version string
|
||||
|
||||
// Commit is the git commit set by ldflags
|
||||
var Commit string
|
||||
|
||||
// IsSetup defines whether we have an admin user or not
|
||||
var IsSetup bool
|
||||
|
||||
// ErrorReporting defines whether we will send errors to Sentry
|
||||
var ErrorReporting bool
|
||||
|
||||
// PublicKey ...
|
||||
var PublicKey string
|
||||
|
||||
// PrivateKey ...
|
||||
var PrivateKey string
|
||||
|
||||
var logLevel logger.Level
|
||||
|
||||
type log struct {
|
||||
Level string `json:"level" envconfig:"optional,default=info"`
|
||||
Format string `json:"format" envconfig:"optional,default=nice"`
|
||||
}
|
||||
|
||||
// Configuration ...
|
||||
var Configuration struct {
|
||||
DataFolder string `json:"data_folder" envconfig:"optional,default=/data"`
|
||||
Log log `json:"log"`
|
||||
}
|
||||
46
backend/internal/database/helpers.go
Normal file
46
backend/internal/database/helpers.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"npm/internal/errors"
|
||||
"npm/internal/model"
|
||||
"npm/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// DateFormat for DateFormat
|
||||
DateFormat = "2006-01-02"
|
||||
// DateTimeFormat for DateTimeFormat
|
||||
DateTimeFormat = "2006-01-02T15:04:05"
|
||||
)
|
||||
|
||||
// GetByQuery returns a row given a query, populating the model given
|
||||
func GetByQuery(model interface{}, query string, params []interface{}) error {
|
||||
db := GetInstance()
|
||||
if db != nil {
|
||||
err := db.Get(model, query, params...)
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// BuildOrderBySQL takes a `Sort` slice and constructs a query fragment
|
||||
func BuildOrderBySQL(columns []string, sort *[]model.Sort) (string, []model.Sort) {
|
||||
var sortStrings []string
|
||||
var newSort []model.Sort
|
||||
for _, sortItem := range *sort {
|
||||
if util.SliceContainsItem(columns, sortItem.Field) {
|
||||
sortStrings = append(sortStrings, fmt.Sprintf("`%s` %s", sortItem.Field, sortItem.Direction))
|
||||
newSort = append(newSort, sortItem)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sortStrings) > 0 {
|
||||
return fmt.Sprintf("ORDER BY %s", strings.Join(sortStrings, ", ")), newSort
|
||||
}
|
||||
|
||||
return "", newSort
|
||||
}
|
||||
38
backend/internal/database/setup.go
Normal file
38
backend/internal/database/setup.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"npm/internal/config"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
// CheckSetup Quick check by counting the number of users in the database
|
||||
func CheckSetup() {
|
||||
query := `SELECT COUNT(*) FROM "user" WHERE is_deleted = $1 and is_disabled = $2`
|
||||
db := GetInstance()
|
||||
|
||||
if db != nil {
|
||||
row := db.QueryRowx(query, false, false)
|
||||
var totalRows int
|
||||
queryErr := row.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
logger.Error("SetupError", queryErr)
|
||||
return
|
||||
}
|
||||
if totalRows == 0 {
|
||||
logger.Warn("No users found, starting in Setup Mode")
|
||||
} else {
|
||||
config.IsSetup = true
|
||||
logger.Info("Application is setup")
|
||||
}
|
||||
|
||||
if config.ErrorReporting {
|
||||
logger.Warn("Error reporting is enabled - Application Errors WILL be sent to Sentry, you can disable this in the Settings interface")
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.Error("DatabaseError", errors.ErrDatabaseUnavailable)
|
||||
}
|
||||
}
|
||||
73
backend/internal/database/sqlite.go
Normal file
73
backend/internal/database/sqlite.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"npm/internal/config"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// Blank import for Sqlite
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var dbInstance *sqlx.DB
|
||||
|
||||
// NewDB creates a new connection
|
||||
func NewDB() {
|
||||
logger.Info("Creating new DB instance")
|
||||
db := SqliteDB()
|
||||
if db != nil {
|
||||
dbInstance = db
|
||||
}
|
||||
}
|
||||
|
||||
// GetInstance returns an existing or new instance
|
||||
func GetInstance() *sqlx.DB {
|
||||
if dbInstance == nil {
|
||||
NewDB()
|
||||
} else if err := dbInstance.Ping(); err != nil {
|
||||
NewDB()
|
||||
}
|
||||
|
||||
return dbInstance
|
||||
}
|
||||
|
||||
// SqliteDB Create sqlite client
|
||||
func SqliteDB() *sqlx.DB {
|
||||
dbFile := fmt.Sprintf("%s/nginxproxymanager.db", config.Configuration.DataFolder)
|
||||
autocreate(dbFile)
|
||||
db, err := sqlx.Open("sqlite3", dbFile)
|
||||
if err != nil {
|
||||
logger.Error("SqliteError", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// Commit will close and reopen the db file
|
||||
func Commit() *sqlx.DB {
|
||||
if dbInstance != nil {
|
||||
err := dbInstance.Close()
|
||||
if err != nil {
|
||||
logger.Error("DatabaseCloseError", err)
|
||||
}
|
||||
}
|
||||
NewDB()
|
||||
return dbInstance
|
||||
}
|
||||
|
||||
func autocreate(dbFile string) {
|
||||
if _, err := os.Stat(dbFile); os.IsNotExist(err) {
|
||||
// Create it
|
||||
logger.Info("Creating Sqlite DB: %s", dbFile)
|
||||
_, err = os.Create(dbFile)
|
||||
if err != nil {
|
||||
logger.Error("FileCreateError", err)
|
||||
}
|
||||
Commit()
|
||||
}
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const error = require('../lib/error');
|
||||
const deadHostModel = require('../models/dead_host');
|
||||
const internalHost = require('./host');
|
||||
const internalNginx = require('./nginx');
|
||||
const internalAuditLog = require('./audit-log');
|
||||
const internalCertificate = require('./certificate');
|
||||
|
||||
function omissions () {
|
||||
return ['is_deleted'];
|
||||
}
|
||||
|
||||
const internalDeadHost = {
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
create: (access, data) => {
|
||||
let create_certificate = data.certificate_id === 'new';
|
||||
|
||||
if (create_certificate) {
|
||||
delete data.certificate_id;
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
data.domain_names.map(function (domain_name) {
|
||||
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
|
||||
});
|
||||
|
||||
return Promise.all(domain_name_check_promises)
|
||||
.then((check_results) => {
|
||||
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);
|
||||
|
||||
return deadHostModel
|
||||
.query()
|
||||
.omit(omissions())
|
||||
.insertAndFetch(data);
|
||||
})
|
||||
.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;
|
||||
}
|
||||
})
|
||||
.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;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Number} data.id
|
||||
* @return {Promise}
|
||||
*/
|
||||
update: (access, data) => {
|
||||
let create_certificate = data.certificate_id === 'new';
|
||||
|
||||
if (create_certificate) {
|
||||
delete data.certificate_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 = [];
|
||||
|
||||
if (typeof data.domain_names !== 'undefined') {
|
||||
data.domain_names.map(function (domain_name) {
|
||||
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id));
|
||||
});
|
||||
|
||||
return Promise.all(domain_name_check_promises)
|
||||
.then((check_results) => {
|
||||
check_results.map(function (result) {
|
||||
if (result.is_taken) {
|
||||
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.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());
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Number} data.id
|
||||
* @param {Array} [data.expand]
|
||||
* @param {Array} [data.omit]
|
||||
* @return {Promise}
|
||||
*/
|
||||
get: (access, data) => {
|
||||
if (typeof data === 'undefined') {
|
||||
data = {};
|
||||
}
|
||||
|
||||
return access.can('dead_hosts:get', data.id)
|
||||
.then((access_data) => {
|
||||
let query = deadHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0)
|
||||
.andWhere('id', data.id)
|
||||
.allowEager('[owner,certificate]')
|
||||
.first();
|
||||
|
||||
if (access_data.permission_visibility !== 'all') {
|
||||
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||
}
|
||||
|
||||
// Custom omissions
|
||||
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||
query.omit(data.omit);
|
||||
}
|
||||
|
||||
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||
query.eager('[' + data.expand.join(', ') + ']');
|
||||
}
|
||||
|
||||
return query;
|
||||
})
|
||||
.then((row) => {
|
||||
if (row) {
|
||||
row = internalHost.cleanRowCertificateMeta(row);
|
||||
return _.omit(row, omissions());
|
||||
} else {
|
||||
throw new error.ItemNotFoundError(data.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Number} data.id
|
||||
* @param {String} [data.reason]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
delete: (access, data) => {
|
||||
return access.can('dead_hosts:delete', data.id)
|
||||
.then(() => {
|
||||
return internalDeadHost.get(access, {id: data.id});
|
||||
})
|
||||
.then((row) => {
|
||||
if (!row) {
|
||||
throw new error.ItemNotFoundError(data.id);
|
||||
}
|
||||
|
||||
return deadHostModel
|
||||
.query()
|
||||
.where('id', row.id)
|
||||
.patch({
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Number} data.id
|
||||
* @param {String} [data.reason]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
enable: (access, data) => {
|
||||
return access.can('dead_hosts:update', data.id)
|
||||
.then(() => {
|
||||
return internalDeadHost.get(access, {
|
||||
id: data.id,
|
||||
expand: ['certificate', 'owner']
|
||||
});
|
||||
})
|
||||
.then((row) => {
|
||||
if (!row) {
|
||||
throw new error.ItemNotFoundError(data.id);
|
||||
} else if (row.enabled) {
|
||||
throw new error.ValidationError('Host is already enabled');
|
||||
}
|
||||
|
||||
row.enabled = 1;
|
||||
|
||||
return deadHostModel
|
||||
.query()
|
||||
.where('id', row.id)
|
||||
.patch({
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
* @param {Number} data.id
|
||||
* @param {String} [data.reason]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
disable: (access, data) => {
|
||||
return access.can('dead_hosts:update', data.id)
|
||||
.then(() => {
|
||||
return internalDeadHost.get(access, {id: data.id});
|
||||
})
|
||||
.then((row) => {
|
||||
if (!row) {
|
||||
throw new error.ItemNotFoundError(data.id);
|
||||
} else if (!row.enabled) {
|
||||
throw new error.ValidationError('Host is already disabled');
|
||||
}
|
||||
|
||||
row.enabled = 0;
|
||||
|
||||
return deadHostModel
|
||||
.query()
|
||||
.where('id', row.id)
|
||||
.patch({
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* All Hosts
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {Array} [expand]
|
||||
* @param {String} [search_query]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAll: (access, expand, search_query) => {
|
||||
return access.can('dead_hosts:list')
|
||||
.then((access_data) => {
|
||||
let query = deadHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0)
|
||||
.groupBy('id')
|
||||
.omit(['is_deleted'])
|
||||
.allowEager('[owner,certificate]')
|
||||
.orderBy('domain_names', 'ASC');
|
||||
|
||||
if (access_data.permission_visibility !== 'all') {
|
||||
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||
}
|
||||
|
||||
// Query is used for searching
|
||||
if (typeof search_query === 'string') {
|
||||
query.where(function () {
|
||||
this.where('domain_names', 'like', '%' + search_query + '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof expand !== 'undefined' && expand !== null) {
|
||||
query.eager('[' + expand.join(', ') + ']');
|
||||
}
|
||||
|
||||
return query;
|
||||
})
|
||||
.then((rows) => {
|
||||
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
|
||||
return internalHost.cleanAllRowsCertificateMeta(rows);
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Report use
|
||||
*
|
||||
* @param {Number} user_id
|
||||
* @param {String} visibility
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCount: (user_id, visibility) => {
|
||||
let query = deadHostModel
|
||||
.query()
|
||||
.count('id as count')
|
||||
.where('is_deleted', 0);
|
||||
|
||||
if (visibility !== 'all') {
|
||||
query.andWhere('owner_user_id', user_id);
|
||||
}
|
||||
|
||||
return query.first()
|
||||
.then((row) => {
|
||||
return parseInt(row.count, 10);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = internalDeadHost;
|
||||
82
backend/internal/entity/auth/methods.go
Normal file
82
backend/internal/entity/auth/methods.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
)
|
||||
|
||||
// GetByID finds a auth by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// GetByUserIDType finds a user by email
|
||||
func GetByUserIDType(userID int, authType string) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByUserIDType(userID, authType)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will create a Auth from this model
|
||||
func Create(auth *Model) (int, error) {
|
||||
if auth.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create auth when model already has an ID")
|
||||
}
|
||||
|
||||
auth.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
user_id,
|
||||
type,
|
||||
secret,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:user_id,
|
||||
:type,
|
||||
:secret,
|
||||
:is_deleted
|
||||
)`, auth)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a Auth from this model
|
||||
func Update(auth *Model) error {
|
||||
if auth.ID == 0 {
|
||||
return goerrors.New("Cannot update auth when model doesn't have an ID")
|
||||
}
|
||||
|
||||
auth.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
user_id = :user_id,
|
||||
type = :type,
|
||||
secret = :secret,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, auth)
|
||||
|
||||
return err
|
||||
}
|
||||
98
backend/internal/entity/auth/model.go
Normal file
98
backend/internal/entity/auth/model.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/types"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "auth"
|
||||
|
||||
// TypePassword is the Password Type
|
||||
TypePassword = "password"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
UserID int `json:"user_id" db:"user_id"`
|
||||
Type string `json:"type" db:"type"`
|
||||
Secret string `json:"secret,omitempty" db:"secret"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// LoadByUserIDType will load from an ID
|
||||
func (m *Model) LoadByUserIDType(userID int, authType string) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE user_id = ? AND type = ? LIMIT 1", tableName)
|
||||
params := []interface{}{userID, authType}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// SetPassword will generate a hashed password based on given string
|
||||
func (m *Model) SetPassword(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost+2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Type = TypePassword
|
||||
m.Secret = string(hash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSecret will check if a given secret matches the encrypted secret
|
||||
func (m *Model) ValidateSecret(secret string) error {
|
||||
if m.Type != TypePassword {
|
||||
return goerrors.New("Could not validate Secret, auth type is not a Password")
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret))
|
||||
if err != nil {
|
||||
return goerrors.New("Invalid Password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
25
backend/internal/entity/certificate/filters.go
Normal file
25
backend/internal/entity/certificate/filters.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"npm/internal/entity"
|
||||
)
|
||||
|
||||
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
|
||||
|
||||
// getFilterMapFunctions is a map of functions that should be executed
|
||||
// during the filtering process, if a field is defined here then the value in
|
||||
// the filter will be given to the defined function and it will return a new
|
||||
// value for use in the sql query.
|
||||
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
|
||||
// if len(filterMapFunctions) == 0 {
|
||||
// TODO: See internal/model/file_item.go:620 for an example
|
||||
// }
|
||||
|
||||
return filterMapFunctions
|
||||
}
|
||||
|
||||
// GetFilterSchema ...
|
||||
func GetFilterSchema() string {
|
||||
var m Model
|
||||
return entity.GetFilterSchema(m)
|
||||
}
|
||||
173
backend/internal/entity/certificate/methods.go
Normal file
173
backend/internal/entity/certificate/methods.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// GetByID finds a row by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will create a row from this model
|
||||
func Create(certificate *Model) (int, error) {
|
||||
if certificate.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create certificate when model already has an ID")
|
||||
}
|
||||
|
||||
certificate.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
user_id,
|
||||
type,
|
||||
certificate_authority_id,
|
||||
dns_provider_id,
|
||||
name,
|
||||
domain_names,
|
||||
expires_on,
|
||||
status,
|
||||
meta,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:user_id,
|
||||
:type,
|
||||
:certificate_authority_id,
|
||||
:dns_provider_id,
|
||||
:name,
|
||||
:domain_names,
|
||||
:expires_on,
|
||||
:status,
|
||||
:meta,
|
||||
:is_deleted
|
||||
)`, certificate)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a Auth from this model
|
||||
func Update(certificate *Model) error {
|
||||
if certificate.ID == 0 {
|
||||
return goerrors.New("Cannot update certificate when model doesn't have an ID")
|
||||
}
|
||||
|
||||
certificate.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
type = :type,
|
||||
user_id = :user_id,
|
||||
certificate_authority_id = :certificate_authority_id,
|
||||
dns_provider_id = :dns_provider_id,
|
||||
name = :name,
|
||||
domain_names = :domain_names,
|
||||
expires_on = :expires_on,
|
||||
status = :status,
|
||||
meta = :meta,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, certificate)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// List will return a list of certificates
|
||||
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
|
||||
var result ListResponse
|
||||
var exampleModel Model
|
||||
|
||||
defaultSort := model.Sort{
|
||||
Field: "name",
|
||||
Direction: "ASC",
|
||||
}
|
||||
|
||||
db := database.GetInstance()
|
||||
if db == nil {
|
||||
return result, errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// Get count of items in this search
|
||||
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
|
||||
countRow := db.QueryRowx(query, params...)
|
||||
var totalRows int
|
||||
queryErr := countRow.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, queryErr
|
||||
}
|
||||
|
||||
// Get rows
|
||||
var items []Model
|
||||
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
|
||||
err := db.Select(&items, query, params...)
|
||||
if err != nil {
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, err
|
||||
}
|
||||
|
||||
result = ListResponse{
|
||||
Items: items,
|
||||
Total: totalRows,
|
||||
Limit: pageInfo.Limit,
|
||||
Offset: pageInfo.Offset,
|
||||
Sort: pageInfo.Sort,
|
||||
Filter: filters,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetByStatus will select rows that are ready for requesting
|
||||
func GetByStatus(status string) ([]Model, error) {
|
||||
models := make([]Model, 0)
|
||||
db := database.GetInstance()
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
t.*
|
||||
FROM "%s" t
|
||||
INNER JOIN "dns_provider" d ON d."id" = t."dns_provider_id"
|
||||
INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id"
|
||||
WHERE
|
||||
t."type" IN ("http", "dns") AND
|
||||
t."status" = ? AND
|
||||
t."certificate_authority_id" > 0 AND
|
||||
t."dns_provider_id" > 0 AND
|
||||
t."is_deleted" = 0
|
||||
`, tableName)
|
||||
|
||||
params := []interface{}{StatusReady}
|
||||
err := db.Select(&models, query, params...)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
logger.Error("GetByStatusError", err)
|
||||
logger.Debug("Query: %s -- %+v", query, params)
|
||||
}
|
||||
|
||||
return models, err
|
||||
}
|
||||
178
backend/internal/entity/certificate/model.go
Normal file
178
backend/internal/entity/certificate/model.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "certificate"
|
||||
|
||||
// TypeCustom ...
|
||||
TypeCustom = "custom"
|
||||
// TypeHTTP ...
|
||||
TypeHTTP = "http"
|
||||
// TypeDNS ...
|
||||
TypeDNS = "dns"
|
||||
// TypeMkcert ...
|
||||
TypeMkcert = "mkcert"
|
||||
// StatusReady is ready for certificate to be requested
|
||||
StatusReady = "ready"
|
||||
// StatusRequesting is process of being requested
|
||||
StatusRequesting = "requesting"
|
||||
// StatusFailed is a certicifate that failed to request
|
||||
StatusFailed = "failed"
|
||||
// StatusProvided is a certificate provided and ready for actual use
|
||||
StatusProvided = "provided"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
ExpiresOn types.NullableDBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"`
|
||||
Type string `json:"type" db:"type" filter:"type,string"`
|
||||
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
|
||||
CertificateAuthorityID int `json:"certificate_authority_id" db:"certificate_authority_id" filter:"certificate_authority_id,integer"`
|
||||
DNSProviderID int `json:"dns_provider_id" db:"dns_provider_id" filter:"dns_provider_id,integer"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"`
|
||||
Status string `json:"status" db:"status" filter:"status,string"`
|
||||
ErrorMessage string `json:"error_message,omitempty" db:"error_message" filter:"error_message,string"`
|
||||
Meta types.JSONB `json:"-" db:"meta"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
// Expansions:
|
||||
CertificateAuthority *certificateauthority.Model `json:"certificate_authority,omitempty"`
|
||||
DNSProvider *dnsprovider.Model `json:"dns_provider,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id, 0}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.UserID == 0 {
|
||||
return fmt.Errorf("User ID must be specified")
|
||||
}
|
||||
|
||||
if !m.Validate() {
|
||||
return fmt.Errorf("Certificate data is incorrect or incomplete for this type")
|
||||
}
|
||||
|
||||
m.setDefaultStatus()
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete will mark a certificate as deleted
|
||||
func (m *Model) Delete() bool {
|
||||
m.Touch(false)
|
||||
m.IsDeleted = true
|
||||
if err := m.Save(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Validate will make sure the data given is expected. This object is a bit complicated,
|
||||
// as there could be multiple combinations of values.
|
||||
func (m *Model) Validate() bool {
|
||||
switch m.Type {
|
||||
case TypeCustom:
|
||||
// TODO: make sure meta contains required fields
|
||||
return m.DNSProviderID == 0 && m.CertificateAuthorityID == 0
|
||||
|
||||
case TypeHTTP:
|
||||
return m.DNSProviderID == 0 && m.CertificateAuthorityID > 0
|
||||
|
||||
case TypeDNS:
|
||||
return m.DNSProviderID > 0 && m.CertificateAuthorityID > 0
|
||||
|
||||
case TypeMkcert:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) setDefaultStatus() {
|
||||
if m.ID == 0 {
|
||||
// It's a new certificate
|
||||
if m.Type == TypeCustom {
|
||||
m.Status = StatusProvided
|
||||
} else {
|
||||
m.Status = StatusReady
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand will populate attached objects for the model
|
||||
func (m *Model) Expand() {
|
||||
if m.CertificateAuthorityID > 0 {
|
||||
certificateAuthority, _ := certificateauthority.GetByID(m.CertificateAuthorityID)
|
||||
m.CertificateAuthority = &certificateAuthority
|
||||
}
|
||||
if m.DNSProviderID > 0 {
|
||||
dnsProvider, _ := dnsprovider.GetByID(m.DNSProviderID)
|
||||
m.DNSProvider = &dnsProvider
|
||||
}
|
||||
}
|
||||
|
||||
// Request ...
|
||||
func (m *Model) Request() error {
|
||||
m.Expand()
|
||||
m.Status = StatusRequesting
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If error
|
||||
m.Status = StatusFailed
|
||||
m.ErrorMessage = "something"
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If done
|
||||
m.Status = StatusProvided
|
||||
t := time.Now()
|
||||
m.ExpiresOn.Time = &t
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
15
backend/internal/entity/certificate/structs.go
Normal file
15
backend/internal/entity/certificate/structs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListResponse is the JSON response for users list
|
||||
type ListResponse struct {
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Sort []model.Sort `json:"sort"`
|
||||
Filter []model.Filter `json:"filter,omitempty"`
|
||||
Items []Model `json:"items,omitempty"`
|
||||
}
|
||||
25
backend/internal/entity/certificateauthority/filters.go
Normal file
25
backend/internal/entity/certificateauthority/filters.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package certificateauthority
|
||||
|
||||
import (
|
||||
"npm/internal/entity"
|
||||
)
|
||||
|
||||
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
|
||||
|
||||
// getFilterMapFunctions is a map of functions that should be executed
|
||||
// during the filtering process, if a field is defined here then the value in
|
||||
// the filter will be given to the defined function and it will return a new
|
||||
// value for use in the sql query.
|
||||
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
|
||||
// if len(filterMapFunctions) == 0 {
|
||||
// TODO: See internal/model/file_item.go:620 for an example
|
||||
// }
|
||||
|
||||
return filterMapFunctions
|
||||
}
|
||||
|
||||
// GetFilterSchema ...
|
||||
func GetFilterSchema() string {
|
||||
var m Model
|
||||
return entity.GetFilterSchema(m)
|
||||
}
|
||||
125
backend/internal/entity/certificateauthority/methods.go
Normal file
125
backend/internal/entity/certificateauthority/methods.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package certificateauthority
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// GetByID finds a row by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will create a row from this model
|
||||
func Create(ca *Model) (int, error) {
|
||||
if ca.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create certificate authority when model already has an ID")
|
||||
}
|
||||
|
||||
ca.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
name,
|
||||
acme2_url,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:name,
|
||||
:acme2_url,
|
||||
:is_deleted
|
||||
)`, ca)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a row from this model
|
||||
func Update(ca *Model) error {
|
||||
if ca.ID == 0 {
|
||||
return goerrors.New("Cannot update certificate authority when model doesn't have an ID")
|
||||
}
|
||||
|
||||
ca.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
name = :name,
|
||||
acme2_url = :acme2_url,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, ca)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// List will return a list of certificates
|
||||
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
|
||||
var result ListResponse
|
||||
var exampleModel Model
|
||||
|
||||
defaultSort := model.Sort{
|
||||
Field: "name",
|
||||
Direction: "ASC",
|
||||
}
|
||||
|
||||
db := database.GetInstance()
|
||||
if db == nil {
|
||||
return result, errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// Get count of items in this search
|
||||
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
|
||||
countRow := db.QueryRowx(query, params...)
|
||||
var totalRows int
|
||||
queryErr := countRow.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
logger.Error("ListCertificateAuthoritiesError", queryErr)
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, queryErr
|
||||
}
|
||||
|
||||
// Get rows
|
||||
var items []Model
|
||||
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
|
||||
err := db.Select(&items, query, params...)
|
||||
if err != nil {
|
||||
logger.Error("ListCertificateAuthoritiesError", err)
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, err
|
||||
}
|
||||
|
||||
result = ListResponse{
|
||||
Items: items,
|
||||
Total: totalRows,
|
||||
Limit: pageInfo.Limit,
|
||||
Offset: pageInfo.Offset,
|
||||
Sort: pageInfo.Sort,
|
||||
Filter: filters,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
67
backend/internal/entity/certificateauthority/model.go
Normal file
67
backend/internal/entity/certificateauthority/model.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package certificateauthority
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "certificate_authority"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
Acme2URL string `json:"acme2_url" db:"acme2_url" filter:"acme2_url,string"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id, 0}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete will mark a certificate as deleted
|
||||
func (m *Model) Delete() bool {
|
||||
m.Touch(false)
|
||||
m.IsDeleted = true
|
||||
if err := m.Save(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
15
backend/internal/entity/certificateauthority/structs.go
Normal file
15
backend/internal/entity/certificateauthority/structs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package certificateauthority
|
||||
|
||||
import (
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListResponse is the JSON response for users list
|
||||
type ListResponse struct {
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Sort []model.Sort `json:"sort"`
|
||||
Filter []model.Filter `json:"filter,omitempty"`
|
||||
Items []Model `json:"items,omitempty"`
|
||||
}
|
||||
25
backend/internal/entity/dnsprovider/filters.go
Normal file
25
backend/internal/entity/dnsprovider/filters.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package dnsprovider
|
||||
|
||||
import (
|
||||
"npm/internal/entity"
|
||||
)
|
||||
|
||||
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
|
||||
|
||||
// getFilterMapFunctions is a map of functions that should be executed
|
||||
// during the filtering process, if a field is defined here then the value in
|
||||
// the filter will be given to the defined function and it will return a new
|
||||
// value for use in the sql query.
|
||||
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
|
||||
// if len(filterMapFunctions) == 0 {
|
||||
// TODO: See internal/model/file_item.go:620 for an example
|
||||
// }
|
||||
|
||||
return filterMapFunctions
|
||||
}
|
||||
|
||||
// GetFilterSchema ...
|
||||
func GetFilterSchema() string {
|
||||
var m Model
|
||||
return entity.GetFilterSchema(m)
|
||||
}
|
||||
131
backend/internal/entity/dnsprovider/methods.go
Normal file
131
backend/internal/entity/dnsprovider/methods.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package dnsprovider
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// GetByID finds a row by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will create a row from this model
|
||||
func Create(provider *Model) (int, error) {
|
||||
if provider.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create dns provider when model already has an ID")
|
||||
}
|
||||
|
||||
provider.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
user_id,
|
||||
provider_key,
|
||||
name,
|
||||
meta,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:user_id,
|
||||
:provider_key,
|
||||
:name,
|
||||
:meta,
|
||||
:is_deleted
|
||||
)`, provider)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a row from this model
|
||||
func Update(provider *Model) error {
|
||||
if provider.ID == 0 {
|
||||
return goerrors.New("Cannot update dns provider when model doesn't have an ID")
|
||||
}
|
||||
|
||||
provider.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
user_id = :user_id,
|
||||
provider_key = :provider_key,
|
||||
name = :name,
|
||||
meta = :meta,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, provider)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// List will return a list of certificates
|
||||
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
|
||||
var result ListResponse
|
||||
var exampleModel Model
|
||||
|
||||
defaultSort := model.Sort{
|
||||
Field: "name",
|
||||
Direction: "ASC",
|
||||
}
|
||||
|
||||
db := database.GetInstance()
|
||||
if db == nil {
|
||||
return result, errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// Get count of items in this search
|
||||
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
|
||||
countRow := db.QueryRowx(query, params...)
|
||||
var totalRows int
|
||||
queryErr := countRow.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
logger.Error("ListDnsProvidersError", queryErr)
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, queryErr
|
||||
}
|
||||
|
||||
// Get rows
|
||||
var items []Model
|
||||
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
|
||||
err := db.Select(&items, query, params...)
|
||||
if err != nil {
|
||||
logger.Error("ListDnsProvidersError", err)
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, err
|
||||
}
|
||||
|
||||
result = ListResponse{
|
||||
Items: items,
|
||||
Total: totalRows,
|
||||
Limit: pageInfo.Limit,
|
||||
Offset: pageInfo.Offset,
|
||||
Sort: pageInfo.Sort,
|
||||
Filter: filters,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
73
backend/internal/entity/dnsprovider/model.go
Normal file
73
backend/internal/entity/dnsprovider/model.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package dnsprovider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "dns_provider"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
|
||||
ProviderKey string `json:"provider_key" db:"provider_key" filter:"provider_key,string"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
Meta types.JSONB `json:"meta" db:"meta"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id, 0}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.UserID == 0 {
|
||||
return fmt.Errorf("User ID must be specified")
|
||||
}
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete will mark a certificate as deleted
|
||||
func (m *Model) Delete() bool {
|
||||
m.Touch(false)
|
||||
m.IsDeleted = true
|
||||
if err := m.Save(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
15
backend/internal/entity/dnsprovider/structs.go
Normal file
15
backend/internal/entity/dnsprovider/structs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package dnsprovider
|
||||
|
||||
import (
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListResponse is the JSON response for the list
|
||||
type ListResponse struct {
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Sort []model.Sort `json:"sort"`
|
||||
Filter []model.Filter `json:"filter,omitempty"`
|
||||
Items []Model `json:"items,omitempty"`
|
||||
}
|
||||
158
backend/internal/entity/filters.go
Normal file
158
backend/internal/entity/filters.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// FilterMapFunction is a filter map function
|
||||
type FilterMapFunction func(value []string) []string
|
||||
|
||||
// FilterTagName ...
|
||||
const FilterTagName = "filter"
|
||||
|
||||
// DBTagName ...
|
||||
const DBTagName = "db"
|
||||
|
||||
// GenerateSQLFromFilters will return a Query and params for use as WHERE clause in SQL queries
|
||||
// This will use a AND where clause approach.
|
||||
func GenerateSQLFromFilters(filters []model.Filter, fieldMap map[string]string, fieldMapFunctions map[string]FilterMapFunction) (string, []interface{}) {
|
||||
clauses := make([]string, 0)
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
for _, filter := range filters {
|
||||
// Lookup this filter field from the functions map
|
||||
if _, ok := fieldMapFunctions[filter.Field]; ok {
|
||||
filter.Value = fieldMapFunctions[filter.Field](filter.Value)
|
||||
}
|
||||
|
||||
// Lookup this filter field from the name map
|
||||
if _, ok := fieldMap[filter.Field]; ok {
|
||||
filter.Field = fieldMap[filter.Field]
|
||||
}
|
||||
|
||||
// Special case for LIKE queries, the column needs to be uppercase for comparison
|
||||
fieldName := fmt.Sprintf("`%s`", filter.Field)
|
||||
if strings.ToLower(filter.Modifier) == "contains" || strings.ToLower(filter.Modifier) == "starts" || strings.ToLower(filter.Modifier) == "ends" {
|
||||
fieldName = fmt.Sprintf("UPPER(`%s`)", filter.Field)
|
||||
}
|
||||
|
||||
clauses = append(clauses, fmt.Sprintf("%s %s", fieldName, getSQLAssignmentFromModifier(filter, ¶ms)))
|
||||
}
|
||||
|
||||
return strings.Join(clauses, " AND "), params
|
||||
}
|
||||
|
||||
func getSQLAssignmentFromModifier(filter model.Filter, params *[]interface{}) string {
|
||||
var clause string
|
||||
|
||||
// Quick hacks
|
||||
if filter.Modifier == "in" && len(filter.Value) == 1 {
|
||||
filter.Modifier = "equals"
|
||||
} else if filter.Modifier == "notin" && len(filter.Value) == 1 {
|
||||
filter.Modifier = "not"
|
||||
}
|
||||
|
||||
switch strings.ToLower(filter.Modifier) {
|
||||
default:
|
||||
clause = "= ?"
|
||||
case "not":
|
||||
clause = "!= ?"
|
||||
case "min":
|
||||
clause = ">= ?"
|
||||
case "max":
|
||||
clause = "<= ?"
|
||||
case "greater":
|
||||
clause = "> ?"
|
||||
case "lesser":
|
||||
clause = "< ?"
|
||||
|
||||
// LIKE modifiers:
|
||||
case "contains":
|
||||
*params = append(*params, strings.ToUpper(filter.Value[0]))
|
||||
return "LIKE '%' || ? || '%'"
|
||||
case "starts":
|
||||
*params = append(*params, strings.ToUpper(filter.Value[0]))
|
||||
return "LIKE ? || '%'"
|
||||
case "ends":
|
||||
*params = append(*params, strings.ToUpper(filter.Value[0]))
|
||||
return "LIKE '%' || ?"
|
||||
|
||||
// Array parameter modifiers:
|
||||
case "in":
|
||||
s, p := buildInArray(filter.Value)
|
||||
*params = append(*params, p...)
|
||||
return fmt.Sprintf("IN (%s)", s)
|
||||
case "notin":
|
||||
s, p := buildInArray(filter.Value)
|
||||
*params = append(*params, p...)
|
||||
return fmt.Sprintf("NOT IN (%s)", s)
|
||||
}
|
||||
|
||||
*params = append(*params, filter.Value[0])
|
||||
return clause
|
||||
}
|
||||
|
||||
// GetFilterMap ...
|
||||
func GetFilterMap(m interface{}) map[string]string {
|
||||
var filterMap = make(map[string]string)
|
||||
|
||||
// TypeOf returns the reflection Type that represents the dynamic type of variable.
|
||||
// If variable is a nil interface value, TypeOf returns nil.
|
||||
t := reflect.TypeOf(m)
|
||||
|
||||
// Iterate over all available fields and read the tag value
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
// Get the field, returns https://golang.org/pkg/reflect/#StructField
|
||||
field := t.Field(i)
|
||||
|
||||
// Get the field tag value
|
||||
filterTag := field.Tag.Get(FilterTagName)
|
||||
dbTag := field.Tag.Get(DBTagName)
|
||||
if filterTag != "" && dbTag != "" && dbTag != "-" && filterTag != "-" {
|
||||
// Filter tag can be a 2 part thing: name,type
|
||||
// ie: account_id,integer
|
||||
// So we need to split and use the first part
|
||||
parts := strings.Split(filterTag, ",")
|
||||
filterMap[parts[0]] = dbTag
|
||||
filterMap[filterTag] = dbTag
|
||||
}
|
||||
}
|
||||
|
||||
return filterMap
|
||||
}
|
||||
|
||||
// GetDBColumns ...
|
||||
func GetDBColumns(m interface{}) []string {
|
||||
var columns []string
|
||||
t := reflect.TypeOf(m)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
dbTag := field.Tag.Get(DBTagName)
|
||||
if dbTag != "" && dbTag != "-" {
|
||||
columns = append(columns, dbTag)
|
||||
}
|
||||
}
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
func buildInArray(items []string) (string, []interface{}) {
|
||||
// Query string placeholder
|
||||
strs := make([]string, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
strs[i] = "?"
|
||||
}
|
||||
|
||||
// Params as interface
|
||||
params := make([]interface{}, len(items))
|
||||
for i, v := range items {
|
||||
params[i] = v
|
||||
}
|
||||
|
||||
return strings.Join(strs, ", "), params
|
||||
}
|
||||
223
backend/internal/entity/filters_schema.go
Normal file
223
backend/internal/entity/filters_schema.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetFilterSchema creates a jsonschema for validating filters, based on the model
|
||||
// object given and by reading the struct "filter" tags.
|
||||
func GetFilterSchema(m interface{}) string {
|
||||
var schemas []string
|
||||
t := reflect.TypeOf(m)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
filterTag := field.Tag.Get(FilterTagName)
|
||||
|
||||
if filterTag != "" && filterTag != "-" {
|
||||
// split out tag value "field,filtreType"
|
||||
// with a default filter type of string
|
||||
items := strings.Split(filterTag, ",")
|
||||
if len(items) == 1 {
|
||||
items = append(items, "string")
|
||||
}
|
||||
|
||||
switch items[1] {
|
||||
case "int":
|
||||
fallthrough
|
||||
case "integer":
|
||||
schemas = append(schemas, intFieldSchema(items[0]))
|
||||
case "bool":
|
||||
fallthrough
|
||||
case "boolean":
|
||||
schemas = append(schemas, boolFieldSchema(items[0]))
|
||||
case "date":
|
||||
schemas = append(schemas, dateFieldSchema(items[0]))
|
||||
case "regex":
|
||||
if len(items) < 3 {
|
||||
items = append(items, ".*")
|
||||
}
|
||||
schemas = append(schemas, regexFieldSchema(items[0], items[2]))
|
||||
|
||||
default:
|
||||
schemas = append(schemas, stringFieldSchema(items[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newFilterSchema(schemas)
|
||||
}
|
||||
|
||||
// newFilterSchema is the main method to specify a new Filter Schema for use in Middleware
|
||||
func newFilterSchema(fieldSchemas []string) string {
|
||||
return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", "))
|
||||
}
|
||||
|
||||
// boolFieldSchema returns the Field Schema for a Boolean accepted value field
|
||||
func boolFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
%s,
|
||||
{
|
||||
"type": "array",
|
||||
"items": %s
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, boolModifiers, filterBool, filterBool)
|
||||
}
|
||||
|
||||
// intFieldSchema returns the Field Schema for a Integer accepted value field
|
||||
func intFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
// stringFieldSchema returns the Field Schema for a String accepted value field
|
||||
func stringFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
%s,
|
||||
{
|
||||
"type": "array",
|
||||
"items": %s
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, stringModifiers, filterString, filterString)
|
||||
}
|
||||
|
||||
// regexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
|
||||
func regexFieldSchema(fieldName string, regex string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "%s"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "%s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, stringModifiers, regex, regex)
|
||||
}
|
||||
|
||||
// dateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
|
||||
func dateFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
const allModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
|
||||
}`
|
||||
|
||||
const boolModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not)$"
|
||||
}`
|
||||
|
||||
const stringModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
|
||||
}`
|
||||
|
||||
const filterBool = `{
|
||||
"type": "string",
|
||||
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
|
||||
}`
|
||||
|
||||
const filterString = `{
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}`
|
||||
|
||||
const baseFilterSchema = `{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
%s
|
||||
]
|
||||
}
|
||||
}`
|
||||
25
backend/internal/entity/host/filters.go
Normal file
25
backend/internal/entity/host/filters.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"npm/internal/entity"
|
||||
)
|
||||
|
||||
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
|
||||
|
||||
// getFilterMapFunctions is a map of functions that should be executed
|
||||
// during the filtering process, if a field is defined here then the value in
|
||||
// the filter will be given to the defined function and it will return a new
|
||||
// value for use in the sql query.
|
||||
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
|
||||
// if len(filterMapFunctions) == 0 {
|
||||
// TODO: See internal/model/file_item.go:620 for an example
|
||||
// }
|
||||
|
||||
return filterMapFunctions
|
||||
}
|
||||
|
||||
// GetFilterSchema ...
|
||||
func GetFilterSchema() string {
|
||||
var m Model
|
||||
return entity.GetFilterSchema(m)
|
||||
}
|
||||
171
backend/internal/entity/host/methods.go
Normal file
171
backend/internal/entity/host/methods.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// GetByID finds a Host by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will create a Host from this model
|
||||
func Create(host *Model) (int, error) {
|
||||
if host.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create host when model already has an ID")
|
||||
}
|
||||
|
||||
host.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
user_id,
|
||||
type,
|
||||
listen_interface,
|
||||
domain_names,
|
||||
upstream_id,
|
||||
certificate_id,
|
||||
access_list_id,
|
||||
ssl_forced,
|
||||
caching_enabled,
|
||||
block_exploits,
|
||||
allow_websocket_upgrade,
|
||||
http2_support,
|
||||
hsts_enabled,
|
||||
hsts_subdomains,
|
||||
paths,
|
||||
upstream_options,
|
||||
advanced_config,
|
||||
is_disabled,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:user_id,
|
||||
:type,
|
||||
:listen_interface,
|
||||
:domain_names,
|
||||
:upstream_id,
|
||||
:certificate_id,
|
||||
:access_list_id,
|
||||
:ssl_forced,
|
||||
:caching_enabled,
|
||||
:block_exploits,
|
||||
:allow_websocket_upgrade,
|
||||
:http2_support,
|
||||
:hsts_enabled,
|
||||
:hsts_subdomains,
|
||||
:paths,
|
||||
:upstream_options,
|
||||
:advanced_config,
|
||||
:is_disabled,
|
||||
:is_deleted
|
||||
)`, host)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a Host from this model
|
||||
func Update(host *Model) error {
|
||||
if host.ID == 0 {
|
||||
return goerrors.New("Cannot update host when model doesn't have an ID")
|
||||
}
|
||||
|
||||
host.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
user_id = :user_id,
|
||||
type = :type,
|
||||
listen_interface = :listen_interface,
|
||||
domain_names = :domain_names,
|
||||
upstream_id = :upstream_id,
|
||||
certificate_id = :certificate_id,
|
||||
access_list_id = :access_list_id,
|
||||
ssl_forced = :ssl_forced,
|
||||
caching_enabled = :caching_enabled,
|
||||
block_exploits = :block_exploits,
|
||||
allow_websocket_upgrade = :allow_websocket_upgrade,
|
||||
http2_support = :http2_support,
|
||||
hsts_enabled = :hsts_enabled,
|
||||
hsts_subdomains = :hsts_subdomains,
|
||||
paths = :paths,
|
||||
upstream_options = :upstream_options,
|
||||
advanced_config = :advanced_config,
|
||||
is_disabled = :is_disabled,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, host)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// List will return a list of hosts
|
||||
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
|
||||
var result ListResponse
|
||||
var exampleModel Model
|
||||
|
||||
defaultSort := model.Sort{
|
||||
Field: "domain_names",
|
||||
Direction: "ASC",
|
||||
}
|
||||
|
||||
db := database.GetInstance()
|
||||
if db == nil {
|
||||
return result, errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// Get count of items in this search
|
||||
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
|
||||
countRow := db.QueryRowx(query, params...)
|
||||
var totalRows int
|
||||
queryErr := countRow.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, queryErr
|
||||
}
|
||||
|
||||
// Get rows
|
||||
var items []Model
|
||||
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
|
||||
err := db.Select(&items, query, params...)
|
||||
if err != nil {
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, err
|
||||
}
|
||||
|
||||
result = ListResponse{
|
||||
Items: items,
|
||||
Total: totalRows,
|
||||
Limit: pageInfo.Limit,
|
||||
Offset: pageInfo.Offset,
|
||||
Sort: pageInfo.Sort,
|
||||
Filter: filters,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
94
backend/internal/entity/host/model.go
Normal file
94
backend/internal/entity/host/model.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "host"
|
||||
|
||||
// ProxyHostType ...
|
||||
ProxyHostType = "proxy"
|
||||
// RedirectionHostType ...
|
||||
RedirectionHostType = "redirection"
|
||||
// DeadHostType ...
|
||||
DeadHostType = "dead"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
|
||||
Type string `json:"type" db:"type" filter:"type,string"`
|
||||
ListenInterface string `json:"listen_interface" db:"listen_interface" filter:"listen_interface,string"`
|
||||
DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"`
|
||||
UpstreamID int `json:"upstream_id" db:"upstream_id" filter:"upstream_id,integer"`
|
||||
CertificateID int `json:"certificate_id" db:"certificate_id" filter:"certificate_id,integer"`
|
||||
AccessListID int `json:"access_list_id" db:"access_list_id" filter:"access_list_id,integer"`
|
||||
SSLForced bool `json:"ssl_forced" db:"ssl_forced" filter:"ssl_forced,boolean"`
|
||||
CachingEnabled bool `json:"caching_enabled" db:"caching_enabled" filter:"caching_enabled,boolean"`
|
||||
BlockExploits bool `json:"block_exploits" db:"block_exploits" filter:"block_exploits,boolean"`
|
||||
AllowWebsocketUpgrade bool `json:"allow_websocket_upgrade" db:"allow_websocket_upgrade" filter:"allow_websocket_upgrade,boolean"`
|
||||
HTTP2Support bool `json:"http2_support" db:"http2_support" filter:"http2_support,boolean"`
|
||||
HSTSEnabled bool `json:"hsts_enabled" db:"hsts_enabled" filter:"hsts_enabled,boolean"`
|
||||
HSTSSubdomains bool `json:"hsts_subdomains" db:"hsts_subdomains" filter:"hsts_subdomains,boolean"`
|
||||
Paths string `json:"paths" db:"paths" filter:"paths,string"`
|
||||
UpstreamOptions string `json:"upstream_options" db:"upstream_options" filter:"upstream_options,string"`
|
||||
AdvancedConfig string `json:"advanced_config" db:"advanced_config" filter:"advanced_config,string"`
|
||||
IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id, 0}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.UserID == 0 {
|
||||
return fmt.Errorf("User ID must be specified")
|
||||
}
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete will mark a host as deleted
|
||||
func (m *Model) Delete() bool {
|
||||
m.Touch(false)
|
||||
m.IsDeleted = true
|
||||
if err := m.Save(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
15
backend/internal/entity/host/structs.go
Normal file
15
backend/internal/entity/host/structs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListResponse is the JSON response for this list
|
||||
type ListResponse struct {
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Sort []model.Sort `json:"sort"`
|
||||
Filter []model.Filter `json:"filter,omitempty"`
|
||||
Items []Model `json:"items,omitempty"`
|
||||
}
|
||||
80
backend/internal/entity/lists_query.go
Normal file
80
backend/internal/entity/lists_query.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListQueryBuilder should be able to return the query and params to get items agnostically based
|
||||
// on given params.
|
||||
func ListQueryBuilder(modelExample interface{}, tableName string, pageInfo *model.PageInfo, defaultSort model.Sort, filters []model.Filter, filterMapFunctions map[string]FilterMapFunction, returnCount bool) (string, []interface{}) {
|
||||
var queryStrings []string
|
||||
var whereStrings []string
|
||||
var params []interface{}
|
||||
|
||||
if returnCount {
|
||||
queryStrings = append(queryStrings, "SELECT COUNT(*)")
|
||||
} else {
|
||||
queryStrings = append(queryStrings, "SELECT *")
|
||||
}
|
||||
|
||||
// nolint: gosec
|
||||
queryStrings = append(queryStrings, fmt.Sprintf("FROM `%s`", tableName))
|
||||
|
||||
// Append filters to where clause:
|
||||
if filters != nil {
|
||||
filterMap := GetFilterMap(modelExample)
|
||||
filterQuery, filterParams := GenerateSQLFromFilters(filters, filterMap, filterMapFunctions)
|
||||
whereStrings = []string{filterQuery}
|
||||
params = append(params, filterParams...)
|
||||
}
|
||||
|
||||
// Add is deletee check if model has the field
|
||||
if hasDeletedField(modelExample) {
|
||||
params = append(params, 0)
|
||||
whereStrings = append(whereStrings, "`is_deleted` = ?")
|
||||
}
|
||||
|
||||
// Append where clauses to query
|
||||
if len(whereStrings) > 0 {
|
||||
// nolint: gosec
|
||||
queryStrings = append(queryStrings, fmt.Sprintf("WHERE %s", strings.Join(whereStrings, " AND ")))
|
||||
}
|
||||
|
||||
if !returnCount {
|
||||
var orderBy string
|
||||
columns := GetDBColumns(modelExample)
|
||||
orderBy, pageInfo.Sort = database.BuildOrderBySQL(columns, &pageInfo.Sort)
|
||||
|
||||
if orderBy != "" {
|
||||
queryStrings = append(queryStrings, orderBy)
|
||||
} else {
|
||||
pageInfo.Sort = append(pageInfo.Sort, defaultSort)
|
||||
queryStrings = append(queryStrings, fmt.Sprintf("ORDER BY `%v` %v", defaultSort.Field, defaultSort.Direction))
|
||||
}
|
||||
|
||||
params = append(params, pageInfo.Offset)
|
||||
params = append(params, pageInfo.Limit)
|
||||
queryStrings = append(queryStrings, "LIMIT ?, ?")
|
||||
}
|
||||
|
||||
return strings.Join(queryStrings, " "), params
|
||||
}
|
||||
|
||||
func hasDeletedField(modelExample interface{}) bool {
|
||||
t := reflect.TypeOf(modelExample)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
dbTag := field.Tag.Get(DBTagName)
|
||||
if dbTag == "is_deleted" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
15
backend/internal/entity/setting/apply.go
Normal file
15
backend/internal/entity/setting/apply.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"npm/internal/config"
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
// ApplySettings will load settings from the DB and apply them where required
|
||||
func ApplySettings() {
|
||||
logger.Debug("Applying Settings")
|
||||
|
||||
// Error-reporting
|
||||
m, _ := GetByName("error-reporting")
|
||||
config.ErrorReporting = m.Value.Decoded.(bool)
|
||||
}
|
||||
25
backend/internal/entity/setting/filters.go
Normal file
25
backend/internal/entity/setting/filters.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"npm/internal/entity"
|
||||
)
|
||||
|
||||
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
|
||||
|
||||
// getFilterMapFunctions is a map of functions that should be executed
|
||||
// during the filtering process, if a field is defined here then the value in
|
||||
// the filter will be given to the defined function and it will return a new
|
||||
// value for use in the sql query.
|
||||
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
|
||||
// if len(filterMapFunctions) == 0 {
|
||||
// TODO: See internal/model/file_item.go:620 for an example
|
||||
// }
|
||||
|
||||
return filterMapFunctions
|
||||
}
|
||||
|
||||
// GetFilterSchema ...
|
||||
func GetFilterSchema() string {
|
||||
var m Model
|
||||
return entity.GetFilterSchema(m)
|
||||
}
|
||||
124
backend/internal/entity/setting/methods.go
Normal file
124
backend/internal/entity/setting/methods.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// GetByID finds a setting by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// GetByName finds a setting by name
|
||||
func GetByName(name string) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByName(name)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will Create a Setting from this model
|
||||
func Create(setting *Model) (int, error) {
|
||||
if setting.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create setting when model already has an ID")
|
||||
}
|
||||
|
||||
setting.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
name,
|
||||
value
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:name,
|
||||
:value
|
||||
)`, setting)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a Setting from this model
|
||||
func Update(setting *Model) error {
|
||||
if setting.ID == 0 {
|
||||
return goerrors.New("Cannot update setting when model doesn't have an ID")
|
||||
}
|
||||
|
||||
setting.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
name = :name,
|
||||
value = :value
|
||||
WHERE id = :id`, setting)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// List will return a list of settings
|
||||
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
|
||||
var result ListResponse
|
||||
var exampleModel Model
|
||||
|
||||
defaultSort := model.Sort{
|
||||
Field: "name",
|
||||
Direction: "ASC",
|
||||
}
|
||||
|
||||
db := database.GetInstance()
|
||||
if db == nil {
|
||||
return result, errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// Get count of items in this search
|
||||
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
|
||||
countRow := db.QueryRowx(query, params...)
|
||||
var totalRows int
|
||||
queryErr := countRow.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
return result, queryErr
|
||||
}
|
||||
|
||||
// Get rows
|
||||
var items []Model
|
||||
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
|
||||
err := db.Select(&items, query, params...)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result = ListResponse{
|
||||
Items: items,
|
||||
Total: totalRows,
|
||||
Limit: pageInfo.Limit,
|
||||
Offset: pageInfo.Offset,
|
||||
Sort: pageInfo.Sort,
|
||||
Filter: filters,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
69
backend/internal/entity/setting/model.go
Normal file
69
backend/internal/entity/setting/model.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "setting"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
Value types.JSONB `json:"value" db:"value"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE `id` = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// LoadByName will load from a Name
|
||||
func (m *Model) LoadByName(name string) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE LOWER(`name`) = ? LIMIT 1", tableName)
|
||||
params := []interface{}{strings.TrimSpace(strings.ToLower(name))}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
// Reapply settings
|
||||
if err == nil {
|
||||
ApplySettings()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
15
backend/internal/entity/setting/structs.go
Normal file
15
backend/internal/entity/setting/structs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListResponse is the JSON response for users list
|
||||
type ListResponse struct {
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Sort []model.Sort `json:"sort"`
|
||||
Filter []model.Filter `json:"filter,omitempty"`
|
||||
Items []Model `json:"items,omitempty"`
|
||||
}
|
||||
25
backend/internal/entity/stream/filters.go
Normal file
25
backend/internal/entity/stream/filters.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"npm/internal/entity"
|
||||
)
|
||||
|
||||
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
|
||||
|
||||
// getFilterMapFunctions is a map of functions that should be executed
|
||||
// during the filtering process, if a field is defined here then the value in
|
||||
// the filter will be given to the defined function and it will return a new
|
||||
// value for use in the sql query.
|
||||
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
|
||||
// if len(filterMapFunctions) == 0 {
|
||||
// TODO: See internal/model/file_item.go:620 for an example
|
||||
// }
|
||||
|
||||
return filterMapFunctions
|
||||
}
|
||||
|
||||
// GetFilterSchema ...
|
||||
func GetFilterSchema() string {
|
||||
var m Model
|
||||
return entity.GetFilterSchema(m)
|
||||
}
|
||||
135
backend/internal/entity/stream/methods.go
Normal file
135
backend/internal/entity/stream/methods.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// GetByID finds a auth by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will create a Auth from this model
|
||||
func Create(host *Model) (int, error) {
|
||||
if host.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create stream when model already has an ID")
|
||||
}
|
||||
|
||||
host.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
user_id,
|
||||
provider,
|
||||
name,
|
||||
domain_names,
|
||||
expires_on,
|
||||
meta,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:user_id,
|
||||
:provider,
|
||||
:name,
|
||||
:domain_names,
|
||||
:expires_on,
|
||||
:meta,
|
||||
:is_deleted
|
||||
)`, host)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a Host from this model
|
||||
func Update(host *Model) error {
|
||||
if host.ID == 0 {
|
||||
return goerrors.New("Cannot update stream when model doesn't have an ID")
|
||||
}
|
||||
|
||||
host.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
user_id = :user_id,
|
||||
provider = :provider,
|
||||
name = :name,
|
||||
domain_names = :domain_names,
|
||||
expires_on = :expires_on,
|
||||
meta = :meta,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, host)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// List will return a list of hosts
|
||||
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
|
||||
var result ListResponse
|
||||
var exampleModel Model
|
||||
|
||||
defaultSort := model.Sort{
|
||||
Field: "name",
|
||||
Direction: "ASC",
|
||||
}
|
||||
|
||||
db := database.GetInstance()
|
||||
if db == nil {
|
||||
return result, errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// Get count of items in this search
|
||||
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
|
||||
countRow := db.QueryRowx(query, params...)
|
||||
var totalRows int
|
||||
queryErr := countRow.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, queryErr
|
||||
}
|
||||
|
||||
// Get rows
|
||||
var items []Model
|
||||
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
|
||||
err := db.Select(&items, query, params...)
|
||||
if err != nil {
|
||||
logger.Debug("%s -- %+v", query, params)
|
||||
return result, err
|
||||
}
|
||||
|
||||
result = ListResponse{
|
||||
Items: items,
|
||||
Total: totalRows,
|
||||
Limit: pageInfo.Limit,
|
||||
Offset: pageInfo.Offset,
|
||||
Sort: pageInfo.Sort,
|
||||
Filter: filters,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
75
backend/internal/entity/stream/model.go
Normal file
75
backend/internal/entity/stream/model.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "stream"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
ExpiresOn types.DBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"`
|
||||
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
|
||||
Provider string `json:"provider" db:"provider" filter:"provider,string"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"`
|
||||
Meta types.JSONB `json:"-" db:"meta"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id, 0}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.UserID == 0 {
|
||||
return fmt.Errorf("User ID must be specified")
|
||||
}
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete will mark a host as deleted
|
||||
func (m *Model) Delete() bool {
|
||||
m.Touch(false)
|
||||
m.IsDeleted = true
|
||||
if err := m.Save(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
15
backend/internal/entity/stream/structs.go
Normal file
15
backend/internal/entity/stream/structs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListResponse is the JSON response for this list
|
||||
type ListResponse struct {
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Sort []model.Sort `json:"sort"`
|
||||
Filter []model.Filter `json:"filter,omitempty"`
|
||||
Items []Model `json:"items,omitempty"`
|
||||
}
|
||||
25
backend/internal/entity/user/filters.go
Normal file
25
backend/internal/entity/user/filters.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"npm/internal/entity"
|
||||
)
|
||||
|
||||
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
|
||||
|
||||
// getFilterMapFunctions is a map of functions that should be executed
|
||||
// during the filtering process, if a field is defined here then the value in
|
||||
// the filter will be given to the defined function and it will return a new
|
||||
// value for use in the sql query.
|
||||
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
|
||||
// if len(filterMapFunctions) == 0 {
|
||||
// TODO: See internal/model/file_item.go:620 for an example
|
||||
// }
|
||||
|
||||
return filterMapFunctions
|
||||
}
|
||||
|
||||
// GetFilterSchema ...
|
||||
func GetFilterSchema() string {
|
||||
var m Model
|
||||
return entity.GetFilterSchema(m)
|
||||
}
|
||||
181
backend/internal/entity/user/methods.go
Normal file
181
backend/internal/entity/user/methods.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// GetByID finds a user by ID
|
||||
func GetByID(id int) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByID(id)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// GetByEmail finds a user by email
|
||||
func GetByEmail(email string) (Model, error) {
|
||||
var m Model
|
||||
err := m.LoadByEmail(email)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Create will create a User from given model
|
||||
func Create(user *Model) (int, error) {
|
||||
// We need to ensure that a user can't be created with the same email
|
||||
// as an existing non-deleted user. Usually you would do this with the
|
||||
// database schema, but it's a bit more complex because of the is_deleted field.
|
||||
|
||||
if user.ID != 0 {
|
||||
return 0, goerrors.New("Cannot create user when model already has an ID")
|
||||
}
|
||||
|
||||
// Check if an existing user with this email exists
|
||||
_, err := GetByEmail(user.Email)
|
||||
if err == nil {
|
||||
return 0, errors.ErrDuplicateEmailUser
|
||||
}
|
||||
|
||||
user.Touch(true)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
|
||||
created_on,
|
||||
modified_on,
|
||||
name,
|
||||
nickname,
|
||||
email,
|
||||
roles,
|
||||
is_disabled
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:name,
|
||||
:nickname,
|
||||
:email,
|
||||
:roles,
|
||||
:is_disabled
|
||||
)`, user)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
last, lastErr := result.LastInsertId()
|
||||
if lastErr != nil {
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
return int(last), nil
|
||||
}
|
||||
|
||||
// Update will Update a User from this model
|
||||
func Update(user *Model) error {
|
||||
if user.ID == 0 {
|
||||
return goerrors.New("Cannot update user when model doesn't have an ID")
|
||||
}
|
||||
|
||||
// Check that the email address isn't associated with another user
|
||||
if existingUser, _ := GetByEmail(user.Email); existingUser.ID != 0 && existingUser.ID != user.ID {
|
||||
return errors.ErrDuplicateEmailUser
|
||||
}
|
||||
|
||||
user.Touch(false)
|
||||
|
||||
db := database.GetInstance()
|
||||
// nolint: gosec
|
||||
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
name = :name,
|
||||
nickname = :nickname,
|
||||
email = :email,
|
||||
roles = :roles,
|
||||
is_disabled = :is_disabled,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, user)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// IsEnabled is used by middleware to ensure the user is still enabled
|
||||
// returns (userExist, isEnabled)
|
||||
func IsEnabled(userID int) (bool, bool) {
|
||||
// nolint: gosec
|
||||
query := `SELECT is_disabled FROM ` + fmt.Sprintf("`%s`", tableName) + ` WHERE id = ? AND is_deleted = ?`
|
||||
disabled := true
|
||||
db := database.GetInstance()
|
||||
err := db.QueryRowx(query, userID, 0).Scan(&disabled)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return false, false
|
||||
} else if err != nil {
|
||||
logger.Error("QueryError", err)
|
||||
}
|
||||
|
||||
return true, !disabled
|
||||
}
|
||||
|
||||
// List will return a list of users
|
||||
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
|
||||
var result ListResponse
|
||||
var exampleModel Model
|
||||
|
||||
defaultSort := model.Sort{
|
||||
Field: "name",
|
||||
Direction: "ASC",
|
||||
}
|
||||
|
||||
db := database.GetInstance()
|
||||
if db == nil {
|
||||
return result, errors.ErrDatabaseUnavailable
|
||||
}
|
||||
|
||||
// Get count of items in this search
|
||||
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
|
||||
logger.Debug("Query: %s -- %+v", query, params)
|
||||
countRow := db.QueryRowx(query, params...)
|
||||
var totalRows int
|
||||
queryErr := countRow.Scan(&totalRows)
|
||||
if queryErr != nil && queryErr != sql.ErrNoRows {
|
||||
return result, queryErr
|
||||
}
|
||||
|
||||
// Get rows
|
||||
var items []Model
|
||||
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
|
||||
logger.Debug("Query: %s -- %+v", query, params)
|
||||
err := db.Select(&items, query, params...)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
for idx := range items {
|
||||
items[idx].generateGravatar()
|
||||
}
|
||||
|
||||
result = ListResponse{
|
||||
Items: items,
|
||||
Total: totalRows,
|
||||
Limit: pageInfo.Limit,
|
||||
Offset: pageInfo.Offset,
|
||||
Sort: pageInfo.Sort,
|
||||
Filter: filters,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteAll will do just that, and should only be used for testing purposes.
|
||||
func DeleteAll() error {
|
||||
db := database.GetInstance()
|
||||
_, err := db.Exec(fmt.Sprintf("DELETE FROM `%s`", tableName))
|
||||
return err
|
||||
}
|
||||
97
backend/internal/entity/user/model.go
Normal file
97
backend/internal/entity/user/model.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity/auth"
|
||||
"npm/internal/types"
|
||||
|
||||
"github.com/drexedam/gravatar"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "user"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
Nickname string `json:"nickname" db:"nickname" filter:"nickname,string"`
|
||||
Email string `json:"email" db:"email" filter:"email,email"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
Roles types.Roles `json:"roles" db:"roles"`
|
||||
GravatarURL string `json:"gravatar_url"`
|
||||
IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
// Expansions
|
||||
Auth *auth.Model `json:"auth,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
err := database.GetByQuery(m, query, params)
|
||||
m.generateGravatar()
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id, false}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// LoadByEmail will load from an Email
|
||||
func (m *Model) LoadByEmail(email string) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE email = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{strings.TrimSpace(strings.ToLower(email)), false}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
m.generateGravatar()
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
// Ensure email is nice
|
||||
m.Email = strings.TrimSpace(strings.ToLower(m.Email))
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete will mark a user as deleted
|
||||
func (m *Model) Delete() bool {
|
||||
m.Touch(false)
|
||||
m.IsDeleted = true
|
||||
if err := m.Save(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) generateGravatar() {
|
||||
m.GravatarURL = gravatar.New(m.Email).
|
||||
Size(128).
|
||||
Default(gravatar.MysteryMan).
|
||||
Rating(gravatar.Pg).
|
||||
AvatarURL()
|
||||
}
|
||||
15
backend/internal/entity/user/structs.go
Normal file
15
backend/internal/entity/user/structs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"npm/internal/model"
|
||||
)
|
||||
|
||||
// ListResponse is the JSON response for users list
|
||||
type ListResponse struct {
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Sort []model.Sort `json:"sort"`
|
||||
Filter []model.Filter `json:"filter,omitempty"`
|
||||
Items []Model `json:"items,omitempty"`
|
||||
}
|
||||
10
backend/internal/errors/errors.go
Normal file
10
backend/internal/errors/errors.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
|
||||
// All error messages used by the service package to report
|
||||
// problems back to calling clients
|
||||
var (
|
||||
ErrDatabaseUnavailable = errors.New("Database is unavailable")
|
||||
ErrDuplicateEmailUser = errors.New("A user already exists with this email address")
|
||||
)
|
||||
@@ -1,235 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const proxyHostModel = require('../models/proxy_host');
|
||||
const redirectionHostModel = require('../models/redirection_host');
|
||||
const deadHostModel = require('../models/dead_host');
|
||||
|
||||
const internalHost = {
|
||||
|
||||
/**
|
||||
* Makes sure that the ssl_* and hsts_* fields play nicely together.
|
||||
* ie: if there is no cert, then force_ssl is off.
|
||||
* if force_ssl is off, then hsts_enabled is definitely off.
|
||||
*
|
||||
* @param {object} data
|
||||
* @param {object} [existing_data]
|
||||
* @returns {object}
|
||||
*/
|
||||
cleanSslHstsData: function (data, existing_data) {
|
||||
existing_data = existing_data === undefined ? {} : existing_data;
|
||||
|
||||
let combined_data = _.assign({}, existing_data, data);
|
||||
|
||||
if (!combined_data.certificate_id) {
|
||||
combined_data.ssl_forced = false;
|
||||
combined_data.http2_support = false;
|
||||
}
|
||||
|
||||
if (!combined_data.ssl_forced) {
|
||||
combined_data.hsts_enabled = false;
|
||||
}
|
||||
|
||||
if (!combined_data.hsts_enabled) {
|
||||
combined_data.hsts_subdomains = false;
|
||||
}
|
||||
|
||||
return combined_data;
|
||||
},
|
||||
|
||||
/**
|
||||
* used by the getAll functions of hosts, this removes the certificate meta if present
|
||||
*
|
||||
* @param {Array} rows
|
||||
* @returns {Array}
|
||||
*/
|
||||
cleanAllRowsCertificateMeta: function (rows) {
|
||||
rows.map(function (row, idx) {
|
||||
if (typeof rows[idx].certificate !== 'undefined' && rows[idx].certificate) {
|
||||
rows[idx].certificate.meta = {};
|
||||
}
|
||||
});
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
/**
|
||||
* used by the get/update functions of hosts, this removes the certificate meta if present
|
||||
*
|
||||
* @param {Object} row
|
||||
* @returns {Object}
|
||||
*/
|
||||
cleanRowCertificateMeta: function (row) {
|
||||
if (typeof row.certificate !== 'undefined' && row.certificate) {
|
||||
row.certificate.meta = {};
|
||||
}
|
||||
|
||||
return row;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param {Array} domain_names
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getHostsWithDomains: function (domain_names) {
|
||||
let promises = [
|
||||
proxyHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0),
|
||||
redirectionHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0),
|
||||
deadHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0)
|
||||
];
|
||||
|
||||
return Promise.all(promises)
|
||||
.then((promises_results) => {
|
||||
let response_object = {
|
||||
total_count: 0,
|
||||
dead_hosts: [],
|
||||
proxy_hosts: [],
|
||||
redirection_hosts: []
|
||||
};
|
||||
|
||||
if (promises_results[0]) {
|
||||
// Proxy Hosts
|
||||
response_object.proxy_hosts = internalHost._getHostsWithDomains(promises_results[0], domain_names);
|
||||
response_object.total_count += response_object.proxy_hosts.length;
|
||||
}
|
||||
|
||||
if (promises_results[1]) {
|
||||
// Redirection Hosts
|
||||
response_object.redirection_hosts = internalHost._getHostsWithDomains(promises_results[1], domain_names);
|
||||
response_object.total_count += response_object.redirection_hosts.length;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal use only, checks to see if the domain is already taken by any other record
|
||||
*
|
||||
* @param {String} hostname
|
||||
* @param {String} [ignore_type] 'proxy', 'redirection', 'dead'
|
||||
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
|
||||
* @returns {Promise}
|
||||
*/
|
||||
isHostnameTaken: function (hostname, ignore_type, ignore_id) {
|
||||
let promises = [
|
||||
proxyHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0)
|
||||
.andWhere('domain_names', 'like', '%' + hostname + '%'),
|
||||
redirectionHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0)
|
||||
.andWhere('domain_names', 'like', '%' + hostname + '%'),
|
||||
deadHostModel
|
||||
.query()
|
||||
.where('is_deleted', 0)
|
||||
.andWhere('domain_names', 'like', '%' + hostname + '%')
|
||||
];
|
||||
|
||||
return Promise.all(promises)
|
||||
.then((promises_results) => {
|
||||
let is_taken = false;
|
||||
|
||||
if (promises_results[0]) {
|
||||
// Proxy Hosts
|
||||
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[0], ignore_type === 'proxy' && ignore_id ? ignore_id : 0)) {
|
||||
is_taken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (promises_results[1]) {
|
||||
// Redirection Hosts
|
||||
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[1], ignore_type === 'redirection' && ignore_id ? ignore_id : 0)) {
|
||||
is_taken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (promises_results[2]) {
|
||||
// Dead Hosts
|
||||
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) {
|
||||
is_taken = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hostname: hostname,
|
||||
is_taken: is_taken
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Private call only
|
||||
*
|
||||
* @param {String} hostname
|
||||
* @param {Array} existing_rows
|
||||
* @param {Integer} [ignore_id]
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) {
|
||||
let is_taken = false;
|
||||
|
||||
if (existing_rows && existing_rows.length) {
|
||||
existing_rows.map(function (existing_row) {
|
||||
existing_row.domain_names.map(function (existing_hostname) {
|
||||
// Does this domain match?
|
||||
if (existing_hostname.toLowerCase() === hostname.toLowerCase()) {
|
||||
if (!ignore_id || ignore_id !== existing_row.id) {
|
||||
is_taken = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return is_taken;
|
||||
},
|
||||
|
||||
/**
|
||||
* Private call only
|
||||
*
|
||||
* @param {Array} hosts
|
||||
* @param {Array} domain_names
|
||||
* @returns {Array}
|
||||
*/
|
||||
_getHostsWithDomains: function (hosts, domain_names) {
|
||||
let response = [];
|
||||
|
||||
if (hosts && hosts.length) {
|
||||
hosts.map(function (host) {
|
||||
let host_matches = false;
|
||||
|
||||
domain_names.map(function (domain_name) {
|
||||
host.domain_names.map(function (host_domain_name) {
|
||||
if (domain_name.toLowerCase() === host_domain_name.toLowerCase()) {
|
||||
host_matches = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (host_matches) {
|
||||
response.push(host);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = internalHost;
|
||||
@@ -1,147 +0,0 @@
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const logger = require('../logger').ip_ranges;
|
||||
const error = require('../lib/error');
|
||||
const internalNginx = require('./nginx');
|
||||
const { Liquid } = require('liquidjs');
|
||||
|
||||
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 internalIpRanges = {
|
||||
|
||||
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
|
||||
interval: null,
|
||||
interval_processing: false,
|
||||
iteration_count: 0,
|
||||
|
||||
initTimer: () => {
|
||||
logger.info('IP Ranges Renewal Timer initialized');
|
||||
internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);
|
||||
},
|
||||
|
||||
fetchUrl: (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info('Fetching ' + url);
|
||||
return https.get(url, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
let raw_data = '';
|
||||
res.on('data', (chunk) => {
|
||||
raw_data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve(raw_data);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggered at startup and then later by a timer, this will fetch the ip ranges from services and apply them to nginx.
|
||||
*/
|
||||
fetch: () => {
|
||||
if (!internalIpRanges.interval_processing) {
|
||||
internalIpRanges.interval_processing = true;
|
||||
logger.info('Fetching IP Ranges from online services...');
|
||||
|
||||
let ip_ranges = [];
|
||||
|
||||
return internalIpRanges.fetchUrl(CLOUDFRONT_URL)
|
||||
.then((cloudfront_data) => {
|
||||
let data = JSON.parse(cloudfront_data);
|
||||
|
||||
if (data && typeof data.prefixes !== 'undefined') {
|
||||
data.prefixes.map((item) => {
|
||||
if (item.service === 'CLOUDFRONT') {
|
||||
ip_ranges.push(item.ip_prefix);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (data && typeof data.ipv6_prefixes !== 'undefined') {
|
||||
data.ipv6_prefixes.map((item) => {
|
||||
if (item.service === 'CLOUDFRONT') {
|
||||
ip_ranges.push(item.ipv6_prefix);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
|
||||
})
|
||||
.then((cloudfare_data) => {
|
||||
let items = cloudfare_data.split('\n');
|
||||
ip_ranges = [... ip_ranges, ... items];
|
||||
})
|
||||
.then(() => {
|
||||
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
|
||||
})
|
||||
.then((cloudfare_data) => {
|
||||
let items = cloudfare_data.split('\n');
|
||||
ip_ranges = [... ip_ranges, ... items];
|
||||
})
|
||||
.then(() => {
|
||||
let clean_ip_ranges = [];
|
||||
ip_ranges.map((range) => {
|
||||
if (range) {
|
||||
clean_ip_ranges.push(range);
|
||||
}
|
||||
});
|
||||
|
||||
return internalIpRanges.generateConfig(clean_ip_ranges)
|
||||
.then(() => {
|
||||
if (internalIpRanges.iteration_count) {
|
||||
// Reload nginx
|
||||
return internalNginx.reload();
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
internalIpRanges.interval_processing = false;
|
||||
internalIpRanges.iteration_count++;
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err.message);
|
||||
internalIpRanges.interval_processing = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array} ip_ranges
|
||||
* @returns {Promise}
|
||||
*/
|
||||
generateConfig: (ip_ranges) => {
|
||||
let renderEngine = new Liquid({
|
||||
root: __dirname + '/../templates/'
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let template = null;
|
||||
let filename = '/etc/nginx/conf.d/include/ip_ranges.conf';
|
||||
try {
|
||||
template = fs.readFileSync(__dirname + '/../templates/ip_ranges.conf', {encoding: 'utf8'});
|
||||
} catch (err) {
|
||||
reject(new error.ConfigurationError(err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
renderEngine
|
||||
.parseAndRender(template, {ip_ranges: ip_ranges})
|
||||
.then((config_text) => {
|
||||
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.warn('Could not write ' + filename + ':', err.message);
|
||||
reject(new error.ConfigurationError(err.message));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = internalIpRanges;
|
||||
60
backend/internal/jwt/jwt.go
Normal file
60
backend/internal/jwt/jwt.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"npm/internal/entity/user"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// UserJWTClaims is the structure of a JWT for a User
|
||||
type UserJWTClaims struct {
|
||||
UserID int `json:"uid"`
|
||||
Roles []string `json:"roles"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// GeneratedResponse is the response of a generated token, usually used in http response
|
||||
type GeneratedResponse struct {
|
||||
Expires int64 `json:"expires"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// Generate will create a JWT
|
||||
func Generate(userObj *user.Model) (GeneratedResponse, error) {
|
||||
var response GeneratedResponse
|
||||
|
||||
key, _ := GetPrivateKey()
|
||||
expires := time.Now().AddDate(0, 0, 1) // 1 day
|
||||
|
||||
// Create the Claims
|
||||
claims := UserJWTClaims{
|
||||
userObj.ID,
|
||||
userObj.Roles,
|
||||
jwt.StandardClaims{
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: expires.Unix(),
|
||||
Issuer: "api",
|
||||
},
|
||||
}
|
||||
|
||||
// Create a new token object, specifying signing method and the claims
|
||||
// you would like it to contain.
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
var err error
|
||||
token.Signature, err = token.SignedString(key)
|
||||
if err != nil {
|
||||
logger.Error("JWTError", fmt.Errorf("Error signing token: %v", err))
|
||||
return response, err
|
||||
}
|
||||
|
||||
response = GeneratedResponse{
|
||||
Expires: expires.Unix(),
|
||||
Token: token.Signature,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
86
backend/internal/jwt/keys.go
Normal file
86
backend/internal/jwt/keys.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
)
|
||||
|
||||
// GetPrivateKey will load the key from config package and return a usable object
|
||||
// It should only load from file once per program execution
|
||||
func GetPrivateKey() (*rsa.PrivateKey, error) {
|
||||
if privateKey == nil {
|
||||
var blankKey *rsa.PrivateKey
|
||||
|
||||
if config.PrivateKey == "" {
|
||||
return blankKey, errors.New("Could not get Private Key from configuration")
|
||||
}
|
||||
|
||||
var err error
|
||||
privateKey, err = LoadPemPrivateKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
return blankKey, err
|
||||
}
|
||||
}
|
||||
|
||||
pub, pubErr := GetPublicKey()
|
||||
if pubErr != nil {
|
||||
return privateKey, pubErr
|
||||
}
|
||||
|
||||
privateKey.PublicKey = *pub
|
||||
|
||||
return privateKey, pubErr
|
||||
}
|
||||
|
||||
// GetPublicKey will load the key from config package and return a usable object
|
||||
// It should only load once per program execution
|
||||
func GetPublicKey() (*rsa.PublicKey, error) {
|
||||
if publicKey == nil {
|
||||
var blankKey *rsa.PublicKey
|
||||
|
||||
if config.PublicKey == "" {
|
||||
return blankKey, errors.New("Could not get Public Key filename, check environment variables")
|
||||
}
|
||||
|
||||
var err error
|
||||
publicKey, err = LoadPemPublicKey(config.PublicKey)
|
||||
if err != nil {
|
||||
return blankKey, err
|
||||
}
|
||||
}
|
||||
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// LoadPemPrivateKey reads a key from a PEM encoded string and returns a private key
|
||||
func LoadPemPrivateKey(content string) (*rsa.PrivateKey, error) {
|
||||
var key *rsa.PrivateKey
|
||||
data, _ := pem.Decode([]byte(content))
|
||||
var err error
|
||||
key, err = x509.ParsePKCS1PrivateKey(data.Bytes)
|
||||
if err != nil {
|
||||
return key, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// LoadPemPublicKey reads a key from a PEM encoded string and returns a public key
|
||||
func LoadPemPublicKey(content string) (*rsa.PublicKey, error) {
|
||||
var key *rsa.PublicKey
|
||||
data, _ := pem.Decode([]byte(content))
|
||||
publicKeyFileImported, err := x509.ParsePKCS1PublicKey(data.Bytes)
|
||||
if err != nil {
|
||||
return key, err
|
||||
}
|
||||
|
||||
return publicKeyFileImported, nil
|
||||
}
|
||||
40
backend/internal/logger/config.go
Normal file
40
backend/internal/logger/config.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package logger
|
||||
|
||||
import "github.com/getsentry/sentry-go"
|
||||
|
||||
// Level type
|
||||
type Level int
|
||||
|
||||
// Log level definitions
|
||||
const (
|
||||
// DebugLevel usually only enabled when debugging. Very verbose logging.
|
||||
DebugLevel Level = 10
|
||||
// InfoLevel general operational entries about what's going on inside the application.
|
||||
InfoLevel Level = 20
|
||||
// WarnLevel non-critical entries that deserve eyes.
|
||||
WarnLevel Level = 30
|
||||
// ErrorLevel used for errors that should definitely be noted.
|
||||
ErrorLevel Level = 40
|
||||
)
|
||||
|
||||
// Config options for the logger.
|
||||
type Config struct {
|
||||
LogThreshold Level
|
||||
Formatter string
|
||||
SentryConfig sentry.ClientOptions
|
||||
}
|
||||
|
||||
// Interface for a logger
|
||||
type Interface interface {
|
||||
GetLogLevel() Level
|
||||
Debug(format string, args ...interface{})
|
||||
Info(format string, args ...interface{})
|
||||
Warn(format string, args ...interface{})
|
||||
Error(errorClass string, err error, args ...interface{})
|
||||
Errorf(errorClass, format string, err error, args ...interface{})
|
||||
}
|
||||
|
||||
// ConfigurableLogger is an interface for a logger that can be configured
|
||||
type ConfigurableLogger interface {
|
||||
Configure(c *Config) error
|
||||
}
|
||||
242
backend/internal/logger/logger.go
Normal file
242
backend/internal/logger/logger.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
var colorReset, colorGray, colorYellow, colorBlue, colorRed, colorMagenta, colorBlack, colorWhite *color.Color
|
||||
|
||||
// Log message structure.
|
||||
type Log struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Pid int `json:"pid"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
StackTrace []string `json:"stack_trace,omitempty"`
|
||||
}
|
||||
|
||||
// Logger instance
|
||||
type Logger struct {
|
||||
Config
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
// global logging configuration.
|
||||
var logger = NewLogger()
|
||||
|
||||
// NewLogger creates a new logger instance
|
||||
func NewLogger() *Logger {
|
||||
color.NoColor = false
|
||||
colorReset = color.New(color.Reset)
|
||||
colorGray = color.New(color.FgWhite)
|
||||
colorYellow = color.New(color.Bold, color.FgYellow)
|
||||
colorBlue = color.New(color.Bold, color.FgBlue)
|
||||
colorRed = color.New(color.Bold, color.FgRed)
|
||||
colorMagenta = color.New(color.Bold, color.FgMagenta)
|
||||
colorBlack = color.New(color.Bold, color.FgBlack)
|
||||
colorWhite = color.New(color.Bold, color.FgWhite)
|
||||
|
||||
return &Logger{
|
||||
Config: NewConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfig returns the default config
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
LogThreshold: InfoLevel,
|
||||
Formatter: "json",
|
||||
}
|
||||
}
|
||||
|
||||
// Configure logger and will return error if missing required fields.
|
||||
func Configure(c *Config) error {
|
||||
return logger.Configure(c)
|
||||
}
|
||||
|
||||
// GetLogLevel currently configured
|
||||
func GetLogLevel() Level {
|
||||
return logger.GetLogLevel()
|
||||
}
|
||||
|
||||
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Debug(format string, args ...interface{}) {
|
||||
logger.Debug(format, args...)
|
||||
}
|
||||
|
||||
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Info(format string, args ...interface{}) {
|
||||
logger.Info(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Warn(format string, args ...interface{}) {
|
||||
logger.Warn(format, args...)
|
||||
}
|
||||
|
||||
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
|
||||
// Attempts to log to bugsang.
|
||||
func Error(errorClass string, err error) {
|
||||
logger.Error(errorClass, err)
|
||||
}
|
||||
|
||||
// Configure logger and will return error if missing required fields.
|
||||
func (l *Logger) Configure(c *Config) error {
|
||||
// ensure updates to the config are atomic
|
||||
l.mux.Lock()
|
||||
defer l.mux.Unlock()
|
||||
|
||||
if c == nil {
|
||||
return fmt.Errorf("a non nil Config is mandatory")
|
||||
}
|
||||
|
||||
if err := c.LogThreshold.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.LogThreshold = c.LogThreshold
|
||||
l.Formatter = c.Formatter
|
||||
l.SentryConfig = c.SentryConfig
|
||||
|
||||
if c.SentryConfig.Dsn != "" {
|
||||
if sentryErr := sentry.Init(c.SentryConfig); sentryErr != nil {
|
||||
fmt.Printf("Sentry initialization failed: %v\n", sentryErr)
|
||||
}
|
||||
}
|
||||
|
||||
stdlog.SetFlags(0) // this removes timestamp prefixes from logs
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate the log level is in the accepted list.
|
||||
func (l Level) validate() error {
|
||||
switch l {
|
||||
case DebugLevel, InfoLevel, WarnLevel, ErrorLevel:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid \"Level\" %d", l)
|
||||
}
|
||||
}
|
||||
|
||||
var logLevels = map[Level]string{
|
||||
DebugLevel: "DEBUG",
|
||||
InfoLevel: "INFO",
|
||||
WarnLevel: "WARN",
|
||||
ErrorLevel: "ERROR",
|
||||
}
|
||||
|
||||
func (l *Logger) logLevel(logLevel Level, format string, args ...interface{}) {
|
||||
if logLevel < l.LogThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
errorClass := ""
|
||||
if logLevel == ErrorLevel {
|
||||
// First arg is the errorClass
|
||||
errorClass = args[0].(string)
|
||||
if len(args) > 1 {
|
||||
args = args[1:]
|
||||
} else {
|
||||
args = []interface{}{}
|
||||
}
|
||||
}
|
||||
|
||||
stringMessage := fmt.Sprintf(format, args...)
|
||||
|
||||
if l.Formatter == "json" {
|
||||
// JSON Log Format
|
||||
jsonLog, _ := json.Marshal(
|
||||
Log{
|
||||
Timestamp: time.Now().Format(time.RFC3339Nano),
|
||||
Level: logLevels[logLevel],
|
||||
Message: stringMessage,
|
||||
Pid: os.Getpid(),
|
||||
},
|
||||
)
|
||||
|
||||
stdlog.Println(string(jsonLog))
|
||||
} else {
|
||||
// Nice Log Format
|
||||
var colorLevel *color.Color
|
||||
switch logLevel {
|
||||
case DebugLevel:
|
||||
colorLevel = colorMagenta
|
||||
case InfoLevel:
|
||||
colorLevel = colorBlue
|
||||
case WarnLevel:
|
||||
colorLevel = colorYellow
|
||||
case ErrorLevel:
|
||||
colorLevel = colorRed
|
||||
stringMessage = fmt.Sprintf("%s: %s", errorClass, stringMessage)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
stdlog.Println(
|
||||
colorBlack.Sprint("["),
|
||||
colorWhite.Sprint(t.Format("2006-01-02 15:04:05")),
|
||||
colorBlack.Sprint("] "),
|
||||
colorLevel.Sprintf("%-8v", logLevels[logLevel]),
|
||||
colorGray.Sprint(stringMessage),
|
||||
colorReset.Sprint(""),
|
||||
)
|
||||
|
||||
if logLevel == ErrorLevel && l.LogThreshold == DebugLevel {
|
||||
// Print a stack trace too
|
||||
debug.PrintStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogLevel currently configured
|
||||
func (l *Logger) GetLogLevel() Level {
|
||||
return l.LogThreshold
|
||||
}
|
||||
|
||||
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||
l.logLevel(DebugLevel, format, args...)
|
||||
}
|
||||
|
||||
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Info(format string, args ...interface{}) {
|
||||
l.logLevel(InfoLevel, format, args...)
|
||||
}
|
||||
|
||||
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Warn(format string, args ...interface{}) {
|
||||
l.logLevel(WarnLevel, format, args...)
|
||||
}
|
||||
|
||||
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
|
||||
// Attempts to log to bugsang.
|
||||
func (l *Logger) Error(errorClass string, err error) {
|
||||
l.logLevel(ErrorLevel, err.Error(), errorClass)
|
||||
l.notifySentry(errorClass, err)
|
||||
}
|
||||
|
||||
func (l *Logger) notifySentry(errorClass string, err error) {
|
||||
if l.SentryConfig.Dsn != "" && l.SentryConfig.Dsn != "-" {
|
||||
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetLevel(sentry.LevelError)
|
||||
scope.SetTag("service", "backend")
|
||||
scope.SetTag("error_class", errorClass)
|
||||
})
|
||||
|
||||
sentry.CaptureException(err)
|
||||
// Since sentry emits events in the background we need to make sure
|
||||
// they are sent before we shut down
|
||||
sentry.Flush(time.Second * 5)
|
||||
}
|
||||
}
|
||||
168
backend/internal/logger/logger_test.go
Normal file
168
backend/internal/logger/logger_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetLogLevel(t *testing.T) {
|
||||
assert.Equal(t, InfoLevel, GetLogLevel())
|
||||
}
|
||||
|
||||
func TestThreshold(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
}))
|
||||
|
||||
Debug("this should not display")
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
Info("this should display")
|
||||
assert.NotEmpty(t, buf.String())
|
||||
|
||||
Error("ErrorClass", errors.New("this should display"))
|
||||
assert.NotEmpty(t, buf.String())
|
||||
}
|
||||
|
||||
func TestDebug(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: DebugLevel,
|
||||
}))
|
||||
|
||||
Debug("This is a %s message", "test")
|
||||
assert.Contains(t, buf.String(), "DEBUG")
|
||||
assert.Contains(t, buf.String(), "This is a test message")
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
}))
|
||||
|
||||
Info("This is a %s message", "test")
|
||||
assert.Contains(t, buf.String(), "INFO")
|
||||
assert.Contains(t, buf.String(), "This is a test message")
|
||||
}
|
||||
|
||||
func TestWarn(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
}))
|
||||
|
||||
Warn("This is a %s message", "test")
|
||||
assert.Contains(t, buf.String(), "WARN")
|
||||
assert.Contains(t, buf.String(), "This is a test message")
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: ErrorLevel,
|
||||
}))
|
||||
|
||||
Error("TestErrorClass", fmt.Errorf("this is a %s error", "test"))
|
||||
assert.Contains(t, buf.String(), "ERROR")
|
||||
assert.Contains(t, buf.String(), "this is a test error")
|
||||
}
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
type args struct {
|
||||
c *Config
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "configure",
|
||||
args: args{
|
||||
&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
SentryConfig: sentry.ClientOptions{},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid log level",
|
||||
args: args{
|
||||
&Config{
|
||||
SentryConfig: sentry.ClientOptions{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Configure(tt.args.c); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Configure() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogLevelBelowThreshold(b *testing.B) {
|
||||
l := NewLogger()
|
||||
|
||||
log.SetOutput(ioutil.Discard)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.logLevel(DebugLevel, "benchmark %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogLevelAboveThreshold(b *testing.B) {
|
||||
l := NewLogger()
|
||||
|
||||
log.SetOutput(ioutil.Discard)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.logLevel(InfoLevel, "benchmark %d", i)
|
||||
}
|
||||
}
|
||||
8
backend/internal/model/filter.go
Normal file
8
backend/internal/model/filter.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
// Filter is the structure of a field/modifier/value item
|
||||
type Filter struct {
|
||||
Field string `json:"field"`
|
||||
Modifier string `json:"modifier"`
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user