more comprehensive data model
This commit is contained in:
parent
1cae332c9c
commit
22b7a1b880
|
@ -1,3 +1,5 @@
|
|||
var/*
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</head>
|
||||
<body class="fade" ng-class="[ 'in' ]">
|
||||
|
||||
<div ng-controller="LoginController as vm" ng-init="vm.setSimple()">
|
||||
<div class="container" ng-controller="LoginController as vm" ng-init="vm.setSimple()">
|
||||
<h1 ng-if="!vm.authnUpdated">Initializing... {{vm.hello}}</h1>
|
||||
<div ng-if="!vm.authnUpdated">
|
||||
<button
|
||||
|
@ -32,8 +32,25 @@
|
|||
ng-click="vm.authenticate()"
|
||||
>Login</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="vm.config">
|
||||
<div class="input-group" ng-repeat="(sitename, siteconf) in vm.config.sites">
|
||||
<label>Hostname:</label> <input class="form-control" ng-model="sitename" />
|
||||
<br/>
|
||||
<div ng-repeat="(pathname, modules) in siteconf.paths">Pathname:
|
||||
<input class="form-control" ng-model="pathname" />
|
||||
<div ng-repeat="(modulename, module) in modules">Modulename: {{modulename}}
|
||||
<div ng-repeat="target in modules">Target:
|
||||
<input class="form-control" ng-model="target" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre><code ng-bind="vm.config | json">{{vm.config}}</code></pre>
|
||||
|
||||
</div>
|
||||
<ul>
|
||||
<li>Show Device Name</li>
|
||||
<li>Login to Daplie</li>
|
||||
|
|
|
@ -67,10 +67,20 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ])
|
|||
, tld: d.tld
|
||||
};
|
||||
})
|
||||
, jwk: null // TODO publish public key
|
||||
}
|
||||
}).then(function (resp) {
|
||||
// TODO resp should contain a token
|
||||
console.info('Initialized Goldilocks', resp);
|
||||
return resp;
|
||||
return OAUTH3.request({
|
||||
method: 'GET'
|
||||
, url: 'https://' + vm.clientUri + '/api/com.daplie.caddy/config'
|
||||
, session: session
|
||||
}).then(function (configResp) {
|
||||
console.log('config', configResp.data);
|
||||
vm.config = configResp.data;
|
||||
return resp;
|
||||
});
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
window.alert("Initialization failed:" + err.message);
|
||||
|
@ -82,6 +92,9 @@ angular.module('com.daplie.cloud', [ 'org.oauth3' ])
|
|||
});
|
||||
};
|
||||
|
||||
oauth3.checkSession().then(function (session) {
|
||||
console.log('hasSession?', session);
|
||||
});
|
||||
|
||||
/*
|
||||
console.log('OAUTH3.PromiseA', OAUTH3.PromiseA);
|
||||
|
|
|
@ -93,10 +93,23 @@ function createServer(port, _delete_me_, content, opts) {
|
|||
return new PromiseA(function (realResolve) {
|
||||
var app = require('../lib/app.js');
|
||||
|
||||
var directive = { content: content, livereload: opts.livereload
|
||||
, sites: opts.sites
|
||||
, assetsPath: opts.assetsPath
|
||||
, expressApp: opts.expressApp };
|
||||
var directive = {
|
||||
content: content
|
||||
, livereload: opts.livereload
|
||||
, global: {
|
||||
greenlock: { email: opts.email, tos: opts.tos }
|
||||
, rvpn: { email: opts.email, tos: opts.tos }
|
||||
, paths: {
|
||||
'/assets/': { serve: [ opts.assetsPath ] }
|
||||
// TODO figure this b out
|
||||
, '/.well-known/': { serve: [ path.resolve(opts.assetsPath, 'well-known') ] }
|
||||
, '/': { serve: [ opts.webRoot ], indexes: [ opts.webRoot ] }
|
||||
}
|
||||
}
|
||||
, sites: opts.sites
|
||||
, expressApp: opts.expressApp
|
||||
};
|
||||
var server;
|
||||
var insecureServer;
|
||||
|
||||
function resolve() {
|
||||
|
@ -160,7 +173,7 @@ function createServer(port, _delete_me_, content, opts) {
|
|||
// Dynamic Certs
|
||||
lex.httpsOptions.SNICallback(sni, cb);
|
||||
};
|
||||
var server = https.createServer(opts.httpsOptions);
|
||||
server = https.createServer(opts.httpsOptions);
|
||||
|
||||
server.on('error', function (err) {
|
||||
if (opts.errorPort || opts.manualPort) {
|
||||
|
@ -369,39 +382,57 @@ function run() {
|
|||
}
|
||||
|
||||
|
||||
opts.sites = [ { name: defaultServername , path: '.' } ];
|
||||
opts.cwd = process.cwd();
|
||||
opts.sites = {};
|
||||
|
||||
if (argv.sites) {
|
||||
opts._externalHost = false;
|
||||
opts.sites = argv.sites.split(',').map(function (name) {
|
||||
argv.sites.split(',').map(function (name) {
|
||||
var nameparts = name.split('|');
|
||||
var servername = nameparts.shift();
|
||||
var modules;
|
||||
|
||||
opts._externalHost = opts._externalHost || !/(^|\.)localhost\./.test(servername);
|
||||
// TODO allow reverse proxy
|
||||
return {
|
||||
name: servername
|
||||
// there should always be a path
|
||||
, paths: nameparts.length && nameparts || [
|
||||
defaultWebRoot.replace(/(:hostname|:servername)/g, servername)
|
||||
]
|
||||
// TODO check for existing custom path before issuing with greenlock
|
||||
, _hasCustomPath: !!nameparts.length
|
||||
};
|
||||
if (!opts.sites[servername]) {
|
||||
opts.sites[servername] = { paths: {} };
|
||||
}
|
||||
|
||||
if (!nameparts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.sites[servername].paths['/']) {
|
||||
opts.sites[servername].paths['/'] = {};
|
||||
}
|
||||
|
||||
modules = opts.sites[servername].paths['/'];
|
||||
modules.serve = nameparts;
|
||||
modules.indexes = nameparts;
|
||||
});
|
||||
}
|
||||
opts.sites.push({
|
||||
name: 'localhost.alpha.daplie.me'
|
||||
, paths: [ path.resolve(__dirname, '..', 'admin', 'public') ]
|
||||
, app: path.join(__dirname, 'admin')
|
||||
});
|
||||
opts.sites.push({
|
||||
name: 'localhost.daplie.invalid'
|
||||
, paths: [ path.join(__dirname, '..', 'admin', 'public') ]
|
||||
, app: path.join(__dirname, 'admin')
|
||||
});
|
||||
|
||||
opts.groups = {};
|
||||
|
||||
// 'packages', 'assets', 'com.daplie.caddy'
|
||||
opts.sites['localhost.alpha.daplie.me'] = {
|
||||
// greenlock: {}
|
||||
paths: {
|
||||
'/': { serve: [ path.resolve(__dirname, '..', 'admin', 'public') ] }
|
||||
, '/api/': { app: path.join(__dirname, 'admin') }
|
||||
}
|
||||
};
|
||||
opts.sites['localhost.daplie.invalid'] = {
|
||||
paths: {
|
||||
'/': { serve: [ path.resolve(__dirname, '..', 'admin', 'public') ] }
|
||||
, '/api/': { app: path.join(__dirname, 'admin') }
|
||||
}
|
||||
};
|
||||
opts.assetsPath = path.join(__dirname, '..', 'packages', 'assets');
|
||||
opts.webRoot = defaultWebRoot;
|
||||
|
||||
// TODO use arrays in all things
|
||||
opts._old_server_name = opts.sites[0].name;
|
||||
opts._old_server_name = Object.keys(opts.sites)[0];
|
||||
opts.pubdir = defaultWebRoot.replace(/(:hostname|:servername).*/, '');
|
||||
|
||||
if (argv.p || argv.port || argv._[0]) {
|
||||
|
|
236
lib/app.js
236
lib/app.js
|
@ -1,52 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function (opts) {
|
||||
var finalhandler = require('finalhandler');
|
||||
var express = require('express');
|
||||
//var finalhandler = require('finalhandler');
|
||||
var serveStatic = require('serve-static');
|
||||
var serveIndex = require('serve-index');
|
||||
var assetServer = serveStatic(opts.assetsPath);
|
||||
//var assetServer = serveStatic(opts.assetsPath);
|
||||
var path = require('path');
|
||||
var wellKnownServer = serveStatic(path.join(opts.assetsPath, 'well-known'));
|
||||
//var wellKnownServer = serveStatic(path.join(opts.assetsPath, 'well-known'));
|
||||
|
||||
var hostsMap = {};
|
||||
var pathsMap = {};
|
||||
var serveStaticMap = {};
|
||||
var serveIndexMap = {};
|
||||
var content = opts.content;
|
||||
var server;
|
||||
//var server;
|
||||
var serveInit;
|
||||
|
||||
function addServer(hostname) {
|
||||
|
||||
if (hostsMap[hostname]) {
|
||||
return hostsMap[hostname];
|
||||
}
|
||||
|
||||
opts.sites.forEach(function (site) {
|
||||
|
||||
if (hostname !== site.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// path should exist before it gets to this point
|
||||
site.path = site.path || site.paths[0];
|
||||
|
||||
if (!pathsMap[site.path]) {
|
||||
console.log('site:', site);
|
||||
pathsMap[site.path] = {
|
||||
serve: serveStatic(site.path)
|
||||
// TODO option for dotfiles
|
||||
, index: serveIndex(site.path)
|
||||
};
|
||||
}
|
||||
|
||||
hostsMap[hostname] = {
|
||||
serve: pathsMap[site.path].serve
|
||||
, index: pathsMap[site.path].index
|
||||
, app: site.app
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
var app;
|
||||
|
||||
function _reloadWrite(data, enc, cb) {
|
||||
/*jshint validthis: true */
|
||||
|
@ -69,14 +37,67 @@ module.exports = function (opts) {
|
|||
}
|
||||
|
||||
|
||||
addServer(opts.sites[0].name);
|
||||
function createServeInit() {
|
||||
var PromiseA = require('bluebird');
|
||||
var fs = PromiseA.promisifyAll(require('fs'));
|
||||
var ownersPath = path.join(__dirname, '..', 'var', 'owners.json');
|
||||
|
||||
return function (req, res) {
|
||||
if ('/api/com.daplie.caddy/init' === req.url) {
|
||||
if (!serveInit) {
|
||||
serveInit = require('../packages/apis/com.daplie.caddy').create(opts);
|
||||
return require('../packages/apis/com.daplie.caddy').create({
|
||||
PromiseA: PromiseA
|
||||
, storage: {
|
||||
owners: {
|
||||
all: function () {
|
||||
var owners;
|
||||
try {
|
||||
owners = require(ownersPath);
|
||||
} catch(e) {
|
||||
owners = {};
|
||||
}
|
||||
|
||||
return PromiseA.resolve(Object.keys(owners).map(function (key) {
|
||||
var owner = owners[key];
|
||||
owner.id = key;
|
||||
return owner;
|
||||
}));
|
||||
}
|
||||
, set: function (id, obj) {
|
||||
var owners;
|
||||
try {
|
||||
owners = require(ownersPath);
|
||||
} catch(e) {
|
||||
owners = {};
|
||||
}
|
||||
obj.id = id;
|
||||
owners[id] = obj;
|
||||
|
||||
return fs.writeFileAsync(ownersPath, JSON.stringify(owners), 'utf8');
|
||||
}
|
||||
}
|
||||
}
|
||||
serveInit(req, res);
|
||||
, recase: require('recase').create({})
|
||||
, options: opts
|
||||
});
|
||||
}
|
||||
|
||||
app = express();
|
||||
|
||||
return app.use('/', function (req, res, next) {
|
||||
if (!req.headers.host) {
|
||||
next(new Error('missing HTTP Host header'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (0 === req.url.indexOf('/api/com.daplie.caddy/')) {
|
||||
if (!serveInit) {
|
||||
serveInit = createServeInit();
|
||||
}
|
||||
}
|
||||
if ('/api/com.daplie.caddy/init' === req.url) {
|
||||
serveInit.init(req, res);
|
||||
return;
|
||||
}
|
||||
if ('/api/com.daplie.caddy/config' === req.url) {
|
||||
serveInit.config(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -85,10 +106,123 @@ module.exports = function (opts) {
|
|||
res.end(content);
|
||||
return;
|
||||
}
|
||||
var done = finalhandler(req, res);
|
||||
var host = req.headers.host;
|
||||
var hostname = (host||'').split(':')[0] || opts.sites[0].name;
|
||||
|
||||
//var done = finalhandler(req, res);
|
||||
var host = req.headers.host;
|
||||
var hostname = (host||'').split(':')[0].toLowerCase();
|
||||
|
||||
console.log('opts.global', opts.global);
|
||||
var sites = [ opts.global || {}, opts.sites[hostname] || {}, opts.defer || {} ];
|
||||
var loadables = {
|
||||
serve: function (config, hostname, pathname, req, res, next) {
|
||||
config = config.slice(0);
|
||||
var originalUrl = req.url;
|
||||
|
||||
function nextServe() {
|
||||
var dirname = config.pop();
|
||||
console.log('[serve]', req.url, hostname, pathname, dirname);
|
||||
if (!dirname) {
|
||||
req.url = originalUrl;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!serveStaticMap[dirname]) {
|
||||
serveStaticMap[dirname] = serveStatic(dirname);
|
||||
}
|
||||
|
||||
serveStaticMap[dirname](req, res, nextServe);
|
||||
}
|
||||
|
||||
req.url = req.url.substr(pathname.length - 1);
|
||||
nextServe();
|
||||
}
|
||||
, indexes: function (config, hostname, pathname, req, res, next) {
|
||||
config = config.slice(0);
|
||||
var originalUrl = req.url;
|
||||
|
||||
function nextIndex() {
|
||||
var dirname = config.pop();
|
||||
console.log('[indexes]', req.url, hostname, pathname, dirname);
|
||||
if (!dirname) {
|
||||
req.url = originalUrl;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!serveStaticMap[dirname]) {
|
||||
serveIndexMap[dirname] = serveIndex(dirname);
|
||||
}
|
||||
serveIndexMap[dirname](req, res, nextIndex);
|
||||
}
|
||||
|
||||
req.url = req.url.substr(pathname.length - 1);
|
||||
nextIndex();
|
||||
}
|
||||
};
|
||||
|
||||
function runModule(config, hostname, pathname, modulename, req, res, next) {
|
||||
if (!loadables[modulename]) {
|
||||
next(new Error("no module '" + modulename + "' found"));
|
||||
return;
|
||||
}
|
||||
loadables[modulename](config, hostname, pathname, req, res, next);
|
||||
}
|
||||
|
||||
function iterModules(modules, hostname, pathname, req, res, next) {
|
||||
var modulenames = Object.keys(modules);
|
||||
|
||||
function nextModule() {
|
||||
var modulename = modulenames.pop();
|
||||
if (!modulename) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('modules', modules);
|
||||
runModule(modules[modulename], hostname, pathname, modulename, req, res, nextModule);
|
||||
}
|
||||
|
||||
nextModule();
|
||||
}
|
||||
|
||||
function iterPaths(site, hostname, req, res, next) {
|
||||
var pathnames = Object.keys(site.paths || {});
|
||||
pathnames = pathnames.filter(function (pathname) {
|
||||
// TODO ensure that pathname has trailing /
|
||||
return (0 === req.url.indexOf(pathname));
|
||||
//return req.url.match(pathname);
|
||||
});
|
||||
pathnames.sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
});
|
||||
|
||||
function nextPath() {
|
||||
var pathname = pathnames.pop();
|
||||
if (!pathname) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('iterPaths', hostname, pathname, req.url);
|
||||
iterModules(site.paths[pathname], hostname, pathname, req, res, nextPath);
|
||||
}
|
||||
|
||||
nextPath();
|
||||
}
|
||||
|
||||
function nextSite() {
|
||||
var site = sites.pop();
|
||||
if (!site) {
|
||||
next(); // 404
|
||||
return;
|
||||
}
|
||||
iterPaths(site, hostname, req, res, nextSite);
|
||||
}
|
||||
|
||||
nextSite();
|
||||
|
||||
/*
|
||||
function serveStaticly(server) {
|
||||
function serveTheStatic() {
|
||||
server.serve(req, res, function (err) {
|
||||
|
@ -129,6 +263,6 @@ module.exports = function (opts) {
|
|||
addServer(hostname);
|
||||
server = hostsMap[hostname] || hostsMap[opts.sites[0].name];
|
||||
serveStaticly(server);
|
||||
|
||||
};
|
||||
*/
|
||||
});
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1",
|
||||
"daplie-tunnel": "git+https://git.daplie.com/Daplie/daplie-cli-tunnel.git#master",
|
||||
"ddns-cli": "git+https://git.daplie.com/Daplie/node-ddns-client.git#master",
|
||||
"express": "git+https://github.com/expressjs/express.git#4.x",
|
||||
"finalhandler": "^0.4.0",
|
||||
"greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master",
|
||||
"greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master",
|
||||
|
@ -55,7 +56,9 @@
|
|||
"localhost.daplie.me-certificates": "^1.3.0",
|
||||
"minimist": "^1.1.1",
|
||||
"oauth3-cli": "git+https://git.daplie.com/OAuth3/oauth3-cli.git#master",
|
||||
"recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4",
|
||||
"redirect-https": "^1.1.0",
|
||||
"scmp": "git+https://github.com/freewil/scmp.git#1.x",
|
||||
"serve-index": "^1.7.0",
|
||||
"serve-static": "^1.10.0",
|
||||
"stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#master"
|
||||
|
|
|
@ -1,18 +1,124 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.create = function (opts) {
|
||||
module.exports.dependencies = [ 'storage.owners' ];
|
||||
module.exports.create = function (deps) {
|
||||
var scmp = require('scmp');
|
||||
var crypto = require('crypto');
|
||||
var jwt = require('jsonwebtoken');
|
||||
var bodyParser = require('body-parser');
|
||||
var jsonParser = bodyParser.json({
|
||||
inflate: true, limit: '100kb', reviver: null, strict: true /* type, verify */
|
||||
});
|
||||
|
||||
return function (req, res) {
|
||||
jsonParser(req, res, function () {
|
||||
|
||||
console.log('req.body', req.body);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json;');
|
||||
res.end(JSON.stringify({ error: { message: "Not Implemented", code: "E_NO_IMPL" } }));
|
||||
/*
|
||||
var owners;
|
||||
deps.storage.owners.on('set', function (_owners) {
|
||||
owners = _owners;
|
||||
});
|
||||
*/
|
||||
deps.storage.owners.exists = function (id) {
|
||||
return deps.storage.owners.all().then(function (owners) {
|
||||
return owners.some(function (owner) {
|
||||
return scmp(id, owner.id);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function isAuthorized(req, res, fn) {
|
||||
var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, ''));
|
||||
if (!auth) {
|
||||
res.setHeader('Content-Type', 'application/json;');
|
||||
res.end(JSON.stringify({ error: { message: "no token", code: 'E_NO_TOKEN', uri: undefined } }));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = crypto.createHash('sha256').update(auth.sub).digest('hex');
|
||||
return deps.storage.owners.exists(id).then(function (exists) {
|
||||
if (!exists) {
|
||||
res.setHeader('Content-Type', 'application/json;');
|
||||
res.end(JSON.stringify({ error: { message: "not authorized", code: 'E_NO_AUTHZ', uri: undefined } }));
|
||||
return;
|
||||
}
|
||||
|
||||
fn();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: function (req, res) {
|
||||
jsonParser(req, res, function () {
|
||||
|
||||
console.log('req.body', req.body);
|
||||
var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, ''));
|
||||
var token = jwt.decode(req.body.access_token);
|
||||
var refresh = jwt.decode(req.body.refresh_token);
|
||||
auth.sub = auth.sub || auth.acx.id;
|
||||
token.sub = token.sub || token.acx.id;
|
||||
refresh.sub = refresh.sub || refresh.acx.id;
|
||||
|
||||
// TODO validate token with issuer, but as-is the sub is already a secret
|
||||
var id = crypto.createHash('sha256').update(auth.sub).digest('hex');
|
||||
var tid = crypto.createHash('sha256').update(token.sub).digest('hex');
|
||||
var rid = crypto.createHash('sha256').update(refresh.sub).digest('hex');
|
||||
|
||||
console.log('ids', id, tid, rid);
|
||||
return deps.storage.owners.all().then(function (results) {
|
||||
console.log('results', results);
|
||||
var err;
|
||||
|
||||
// There is no owner yet. First come, first serve.
|
||||
if (!results || !results.length) {
|
||||
if (tid !== id || rid !== id) {
|
||||
err = new Error("When creating an owner the Authorization Bearer and Token and Refresh must all match");
|
||||
return deps.PromiseA.reject(err);
|
||||
}
|
||||
console.log('no owner, creating');
|
||||
return deps.storage.owners.set(id, { token: token, refresh: refresh });
|
||||
}
|
||||
console.log('has results');
|
||||
|
||||
// There are onwers. Is this one of them?
|
||||
if (!results.some(function (token) {
|
||||
return scmp(id, token.id);
|
||||
})) {
|
||||
err = new Error("Authorization token does not belong to an existing owner.");
|
||||
return deps.PromiseA.reject(err);
|
||||
}
|
||||
console.log('has correct owner');
|
||||
|
||||
// We're adding an owner, unless it already exists
|
||||
if (!results.some(function (token) {
|
||||
return scmp(tid, token.id);
|
||||
})) {
|
||||
console.log('adds new owner with existing owner');
|
||||
return deps.storage.owners.set(id, { token: token, refresh: refresh });
|
||||
}
|
||||
}).then(function () {
|
||||
res.setHeader('Content-Type', 'application/json;');
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
}, function (err) {
|
||||
res.setHeader('Content-Type', 'application/json;');
|
||||
res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } }));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
, config: function (req, res) {
|
||||
isAuthorized(req, res, function () {
|
||||
if ('POST' !== req.method) {
|
||||
res.setHeader('Content-Type', 'application/json;');
|
||||
res.end(JSON.stringify(deps.recase.snakeCopy(deps.options)));
|
||||
return;
|
||||
}
|
||||
|
||||
jsonParser(req, res, function () {
|
||||
|
||||
console.log('req.body', req.body);
|
||||
|
||||
deps.storage.config.merge(req.body);
|
||||
deps.storage.config.save();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit eff1fd11272472e8f6d03ac8b897a9853810672e
|
||||
Subproject commit f179cfe3c9553718676db24f1203f67ea0427662
|
Loading…
Reference in New Issue