diff --git a/app/controllers/admin/service-node.js b/app/controllers/admin/service-node.js index da48c81..c644026 100644 --- a/app/controllers/admin/service-node.js +++ b/app/controllers/admin/service-node.js @@ -23,9 +23,12 @@ class ServiceNodeController extends SiteController { }); router.param('clientId', this.populateClientId.bind(this)); + router.param('connectRequestId', this.populateConnectRequestId.bind(this)); + router.post('/connect-queue/:connectRequestId', this.postConnectRequestResponse.bind(this)); router.post('/:clientId', this.postClientUpdate.bind(this)); + router.get('/connect-queue', this.getServiceNodeConnectQueue.bind(this)); router.get('/:clientId', this.getClientView.bind(this)); router.get('/', this.getIndex.bind(this)); @@ -46,6 +49,25 @@ class ServiceNodeController extends SiteController { } } + async populateConnectRequestId (req, res, next, connectRequestId) { + const { coreNode: coreNodeService } = this.dtp.services; + try { + res.locals.coreConnect = await coreNodeService.getCoreConnectRequest(connectRequestId); + if (!res.locals.coreConnect) { + throw new SiteError(404, 'Core Connect request not found'); + } + return next(); + } catch (error) { + this.log.error('failed to populate connectRequestId', { connectRequestId, error }); + return next(error); + } + } + + async postConnectRequestResponse (req, res/*, next*/) { + this.log.info('processing Core Connect response', { coreConnect: res.locals.coreConnect, action: req.body.action }); + res.status(200).json({ success: true, coreConnect: res.locals.coreConnect }); + } + async postClientUpdate (req, res, next) { const { oauth2: oauth2Service } = this.dtp.services; try { @@ -57,6 +79,18 @@ class ServiceNodeController extends SiteController { } } + async getServiceNodeConnectQueue (req, res, next) { + const { coreNode: coreNodeService } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.connectQueue = await coreNodeService.getServiceNodeQueue(res.locals.pagination); + res.render('admin/service-node/connect-queue'); + } catch (error) { + this.log.error('failed to render Core Connect queue', { error }); + return next(error); + } + } + async getClientView (req, res) { res.render('admin/service-node/editor'); } diff --git a/app/controllers/hive.js b/app/controllers/hive.js index 682298a..df595b7 100644 --- a/app/controllers/hive.js +++ b/app/controllers/hive.js @@ -33,7 +33,7 @@ class HiveController extends SiteController { }, ); - router.use('/kaleidoscope',await this.loadChild(path.join(__dirname, 'hive', 'kaleidoscope'))); + router.use('/kaleidoscope', await this.loadChild(path.join(__dirname, 'hive', 'kaleidoscope'))); this.services.push({ name: 'kaleidoscope', url: '/hive/kaleidoscope' }); router.get('/', this.getHiveRoot.bind(this)); diff --git a/app/models/core-node-connect.js b/app/models/core-node-connect.js new file mode 100644 index 0000000..599503e --- /dev/null +++ b/app/models/core-node-connect.js @@ -0,0 +1,33 @@ +// core-node-connect.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const CONNECT_STATUS_LIST = ['pending', 'accepted', 'rejected']; + +const CoreNodeConnectSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: 1, expires: '30d' }, + token: { type: String, required: true }, + status: { type: String, enum: CONNECT_STATUS_LIST, default: 'pending', required: true, index: true }, + pkg: { + name: { type: String, required: true }, + version: { type: String, required: true }, + }, + site: { + domain: { type: String, required: true, index: 1 }, + domainKey: { type: String, required: true, lowercase: true, index: 1 }, + name: { type: String, required: true }, + description: { type: String, required: true }, + company: { type: String, required: true }, + coreAuth: { + scopes: { type: [String], required: true }, + callbackUrl: { type: String, required: true }, + }, + }, +}); + +module.exports = mongoose.model('CoreNodeConnect', CoreNodeConnectSchema); \ No newline at end of file diff --git a/app/services/core-node.js b/app/services/core-node.js index aaa3001..9f45068 100644 --- a/app/services/core-node.js +++ b/app/services/core-node.js @@ -11,6 +11,8 @@ const mongoose = require('mongoose'); const CoreNode = mongoose.model('CoreNode'); const CoreUser = mongoose.model('CoreUser'); + +const CoreNodeConnect = mongoose.model('CoreNodeConnect'); const CoreNodeRequest = mongoose.model('CoreNodeRequest'); const passport = require('passport'); @@ -416,6 +418,79 @@ class CoreNodeService extends SiteService { ); } + async queueServiceNodeConnect (requestToken, appNode) { + const NOW = new Date(); + + appNode.site.domain = striptags(appNode.site.domain.trim().toLowerCase()); + appNode.site.domainKey = striptags(appNode.site.domainKey.trim().toLowerCase()); + appNode.site.name = striptags(appNode.site.name.trim()); + appNode.site.description = striptags(appNode.site.description.trim()); + appNode.site.company = striptags(appNode.site.company.trim()); + appNode.site.coreAuth.scopes = appNode.site.coreAuth.scopes.map((scope) => scope.trim().toLowerCase()); + appNode.site.coreAuth.callbackUrl = striptags(appNode.site.coreAuth.callbackUrl.trim()); + + let request = await CoreNodeConnect.findOne({ + $or: [ + { domain: appNode.site.domain }, + { domainKey: appNode.site.domainKey }, + ], + }); + if (request) { + throw new SiteError(406, 'You already have a pending Core Connect request'); + } + + request = new CoreNodeConnect(); + request.created = NOW; + request.token = requestToken; + request.status = 'pending'; + request.pkg = { + name: appNode.pkg.name, + version: appNode.pkg.version, + }; + request.site = { + domain: appNode.site.domain, + domainKey: appNode.site.domainKey, + name: appNode.site.name, + description: appNode.site.description, + company: appNode.site.company, + coreAuth: { + scopes: appNode.site.coreAuth.scopes, + callbackUrl: appNode.site.coreAuth.callbackUrl, + }, + }; + + await request.save(); + + return request.toObject(); + } + + async getServiceNodeQueue (pagination) { + const queue = await CoreNodeConnect + .find({ status: 'pending' }) + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .lean(); + return queue; + } + + async getCoreConnectRequest (requestId) { + const request = await CoreNodeConnect + .findOne({ _id: requestId }) + .lean(); + return request; + } + + async acceptServiceNode (requestToken, appNode) { + const { oauth2: oauth2Service } = this.dtp.services; + const response = { token: requestToken }; + + this.log.info('accepting app node', { requestToken, appNode }); + response.client = await oauth2Service.createClient(appNode.site); + + return response; + } + async setCoreOAuth2Credentials (core, credentials) { const { client } = credentials; this.log.info('updating Core Connect credentials', { core, client }); diff --git a/app/views/admin/components/menu.pug b/app/views/admin/components/menu.pug index 7dd8fbb..17a8738 100644 --- a/app/views/admin/components/menu.pug +++ b/app/views/admin/components/menu.pug @@ -44,6 +44,12 @@ ul.uk-nav.uk-nav-default i.fas.fa-puzzle-piece span.uk-margin-small-left Service Nodes + li(class={ 'uk-active': (adminView === 'connect-queue') }) + a(href="/admin/service-node/connect-queue") + span.nav-item-icon + i.fas.fa-plug + span.uk-margin-small-left Connect Queue + li(class={ 'uk-active': (adminView === 'host') }) a(href="/admin/host") span.nav-item-icon diff --git a/app/views/admin/service-node/connect-queue.pug b/app/views/admin/service-node/connect-queue.pug new file mode 100644 index 0000000..57a17df --- /dev/null +++ b/app/views/admin/service-node/connect-queue.pug @@ -0,0 +1,41 @@ +extends ../layouts/main +block content + + h1 Service Node Connect Queue + + if Array.isArray(connectQueue) && (connectQueue.length > 0) + table.uk-table.uk-table-small + thead + tr + th Actions + th Name + th Domain + th Key + th Received + tbody + each connectRequest in connectQueue + tr + td + button( + type="button", + data-request-id= connectRequest._id, + onclick="return dtp.adminApp.postCoreConnectResponse(event, 'approve');", + ).uk-button.uk-button-default.uk-button-small.uk-border-rounded + span + i.fas.fa-check + + button( + type="button", + data-request-id= connectRequest._id, + onclick="return dtp.adminApp.postCoreConnectResponse(event, 'reject');" + ).uk-button.uk-button-danger.uk-button-small.uk-border-rounded + span + i.fas.fa-times + td + - var CORE_SCHEME = process.env.DTP_CORE_AUTH_SCHEME || 'https'; + a(href=`${CORE_SCHEME}://${connectRequest.site.domain}/`, target="_blank")= connectRequest.site.name + td= connectRequest.site.domain + td= connectRequest.site.domainKey + td= moment(connectRequest.created).fromNow() + else + div The pending Core Connect queue is empty. \ No newline at end of file diff --git a/client/js/site-admin-app.js b/client/js/site-admin-app.js index 4c0c4a4..8b2fff8 100644 --- a/client/js/site-admin-app.js +++ b/client/js/site-admin-app.js @@ -286,6 +286,25 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { UIkit.modal.alert(`Failed to delete post: ${error.message}`); } } + + async postCoreConnectResponse (event, action) { + const target = event.currentTarget || event.target; + const requestId = target.getAttribute('data-request-id'); + try { + this.log.info('postCoreConnectResponse', 'posting Core Connect response', { requestId, action }); + const requestUrl = `/admin/service-node/connect-queue/${requestId}`; + const response = await fetch(requestUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action }), + }); + await this.processResponse(response); + } catch (error) { + UIkit.modal.alert(`Failed to send Core Connect response: ${error.message}`); + } + } } dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp; \ No newline at end of file