diff --git a/app/controllers/admin/core-node.js b/app/controllers/admin/core-node.js index 2ae9816..fd7cdc9 100644 --- a/app/controllers/admin/core-node.js +++ b/app/controllers/admin/core-node.js @@ -33,6 +33,8 @@ class CoreNodeController extends SiteController { router.get('/:coreNodeId', this.getCoreNodeView.bind(this)); router.get('/', this.getIndex.bind(this)); + router.delete('/:coreNodeId', this.deleteCoreNode.bind(this)); + return router; } @@ -137,6 +139,23 @@ class CoreNodeController extends SiteController { return next(error); } } + + async deleteCoreNode (req, res) { + const { coreNode: coreNodeService } = this.dtp.services; + try { + await coreNodeService.disconnect(res.locals.coreNode); + + const displayList = this.createDisplayList('core-disconnect'); + displayList.navigateTo('/admin/core-node'); + res.status(200).json({ success: true, displayList }); + } catch (error) { + this.log.error('failed to disconnect from Core', { error }); + return res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } } module.exports = { diff --git a/app/models/kaleidoscope-event.js b/app/models/kaleidoscope-event.js index c24e98e..b76f33b 100644 --- a/app/models/kaleidoscope-event.js +++ b/app/models/kaleidoscope-event.js @@ -20,6 +20,7 @@ const KaleidoscopeEventSchema = new Schema({ href: { type: String }, thumbnail: { type: String }, source: { + client: { type: Schema.ObjectId, index: 1, ref: 'OAuth2Client' }, pkg: { name: { type: String, required: true }, version: { type: String, required: true }, diff --git a/app/models/lib/user-types.js b/app/models/lib/user-types.js index 7d7d7d3..94c8d60 100644 --- a/app/models/lib/user-types.js +++ b/app/models/lib/user-types.js @@ -9,11 +9,17 @@ const Schema = mongoose.Schema; module.exports.DTP_THEME_LIST = ['dtp-light', 'dtp-dark']; +module.exports.DTP_USER_TYPE_LIST = ['CoreUser', 'User']; +module.exports.DtpUserSchema = new Schema({ + userType: { type: String, enum: module.exports.DTP_USER_TYPE_LIST, required: true }, + user: { type: Schema.ObjectId, required: true, index: true, refPath: 'userType' }, +}, { _id: false }); + module.exports.UserFlagsSchema = new Schema({ isAdmin: { type: Boolean, default: false, required: true }, isModerator: { type: Boolean, default: false, required: true }, isEmailVerified: { type: Boolean, default: false, required: true }, -}); +}, { _id: false }); module.exports.UserPermissionsSchema = new Schema({ canLogin: { type: Boolean, default: true, required: true }, @@ -24,9 +30,9 @@ module.exports.UserPermissionsSchema = new Schema({ canAuthorPosts: { type: Boolean, default: false, required: true }, canPublishPages: { type: Boolean, default: false, required: true }, canPublishPosts: { type: Boolean, default: false, required: true }, -}); +}, { _id: false }); module.exports.UserOptInSchema = new Schema({ system: { type: Boolean, default: true, required: true }, marketing: { type: Boolean, default: true, required: true }, -}); \ No newline at end of file +}, { _id: false }); \ No newline at end of file diff --git a/app/models/user-block.js b/app/models/user-block.js index 2046267..f6396e1 100644 --- a/app/models/user-block.js +++ b/app/models/user-block.js @@ -7,9 +7,11 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; +const { DtpUserSchema } = require('./lib/user-types.js'); + const UserBlockSchema = new Schema({ - user: { type: Schema.ObjectId, required: true, index: true, ref: 'User' }, - blockedUsers: { type: [Schema.ObjectId], ref: 'User' }, + member: { type: DtpUserSchema, required: true }, + blockedMembers: { type: [DtpUserSchema] }, }); module.exports = mongoose.model('UserBlock', UserBlockSchema); \ No newline at end of file diff --git a/app/services/core-node.js b/app/services/core-node.js index 923fd37..0fac7ad 100644 --- a/app/services/core-node.js +++ b/app/services/core-node.js @@ -61,6 +61,12 @@ class CoreNodeService extends SiteService { async start ( ) { await super.start(); + const https = require('https'); + this.httpsAgent = new https.Agent({ + // read it out-loud: Reject unauthorized when not the 'local' environment. + rejectUnauthorized: (process.env.NODE_ENV !== 'local'), + }); + const cores = await this.getConnectedCores(null, true); this.log.info('Core Node service starting', { connectedCoreCount: cores.length }); @@ -187,7 +193,7 @@ class CoreNodeService extends SiteService { url: '/core/info/package', }); - await CoreNode.updateOne( + core = await CoreNode.findOneAndUpdate( { _id: core._id }, { $set: { @@ -201,10 +207,11 @@ class CoreNodeService extends SiteService { 'meta.supportEmail': txSite.response.site.supportEmail, }, }, + { new: true }, ); - core = await CoreNode.findOne({ _id: core._id }).lean(); this.log.info('resolved Core node', { core }); + this.emitDtpEvent('resolve', { core, host }); return { core, networkPolicy: txSite.response.site.networkPolicy }; } @@ -257,6 +264,8 @@ class CoreNodeService extends SiteService { body: { event }, }; + await this.emitDtpEvent('kaleidoscope.event', { event, recipients, request }); + if (!recipients) { return this.broadcast(request); } @@ -303,6 +312,7 @@ class CoreNodeService extends SiteService { async broadcast (request) { const results = [ ]; + await this.emitDtpEvent('kaleidoscope.broadcast', { request }); await CoreNode .find({ 'flags.isConnected': true, @@ -327,6 +337,7 @@ class CoreNodeService extends SiteService { try { const req = new CoreNodeRequest(); const options = { + agent: this.httpsAgent, headers: { 'Content-Type': 'application/json', }, @@ -353,8 +364,10 @@ class CoreNodeService extends SiteService { options.body = JSON.stringify(request.body); } - this.log.info('sending Core node request', { request: req }); const requestUrl = this.getCoreRequestUrl(core, request.url); + await this.emitDtpEvent('kaleidoscope.request', { core, request, requestUrl }); + + this.log.info('sending Core node request', { request: req }); const response = await fetch(requestUrl, options); if (!response.ok) { let json; @@ -384,6 +397,10 @@ class CoreNodeService extends SiteService { async setRequestResponse (request, response, json) { const DONE = new Date(); const ELAPSED = DONE.valueOf() - request.created.valueOf(); + + /* + * Build the default update operation + */ const updateOp = { $set: { 'response.received': DONE, @@ -391,9 +408,20 @@ class CoreNodeService extends SiteService { 'response.statusCode': response.status, }, }; + if (json) { updateOp.$set['response.success'] = json.success; } + + /* + * Provide an opportunity for anything to alter the operation or cancel it. + */ + await this.emitDtpEvent('kaleidoscope.response', { + core: request.core, + request, response, json, + updateOp, + }); + await CoreNodeRequest.updateOne({ _id: request._id }, updateOp); } @@ -433,6 +461,52 @@ class CoreNodeService extends SiteService { }, }, ); + + await this.emitDtpEvent('connect', { core: request.core, request }); + } + + async disconnect (core) { + this.log.alert('disconnecting from Core', { + name: core.meta.name, + domain: core.meta.domain, + }); + + // provides an abort point if any listener throws + await this.emitDtpEvent('disconnect-pre', { core }); + + const disconnect = await this.sendRequest(core, { + method: 'DELETE', + url: `/core/connect/node/${core.oauth.clientId}`, + }); + this.log.alert('Core disconnect request complete', { + name: core.meta.name, + domain: core.meta.domain, + disconnect, + }); + + try { + await this.emitDtpEvent('disconnect-post', { core, disconnect }); + } catch (error) { + this.log.error('failed to emit dtp.core.disconnect-post', { error }); + // keep going + } + + await CoreUser + .find({ core: core._id }) + .cursor() + .eachAsync(this.removeUser.bind(this, core), 1); + + // await CoreNodeConnect.deleteMany({ 'site.domainKey': core.meta.domainKey }); + // await CoreNodeRequest.deleteMany({ core: core._id }); + + try { + await this.emitDtpEvent('disconnect', { core, disconnect }); + } catch (error) { + this.log.error('failed to emit dtp.core.disconnect', { error }); + // keep going + } + + return disconnect; } async queueServiceNodeConnect (requestToken, appNode) { @@ -478,7 +552,10 @@ class CoreNodeService extends SiteService { await request.save(); - return request.toObject(); + request = request.toObject(); + await this.emitDtpEvent('service-node.connect', { request }); + + return request; } async getServiceNodeQueue (pagination) { @@ -498,7 +575,6 @@ class CoreNodeService extends SiteService { return request; } - async acceptServiceNode (requestToken, appNode) { const { oauth2: oauth2Service } = this.dtp.services; const response = { token: requestToken }; @@ -506,13 +582,15 @@ class CoreNodeService extends SiteService { this.log.info('accepting app node', { requestToken, appNode }); response.client = await oauth2Service.createClient(appNode.site); + await this.emitDtpEvent('service-node.accept', { client: response.client }); + return response; } async setCoreOAuth2Credentials (core, credentials) { const { client } = credentials; this.log.info('updating Core Connect credentials', { core, client }); - await CoreNode.updateOne( + core = await CoreNode.findOneAndUpdate( { _id: core._id }, { $set: { @@ -524,7 +602,9 @@ class CoreNodeService extends SiteService { 'kaleidoscope.token': client.kaleidoscope.token, }, }, + { new: true }, ); + await this.emitDtpEvent('set-oauth2-credentials', { core }); } registerPassportCoreOAuth2 (core) { @@ -591,6 +671,7 @@ class CoreNodeService extends SiteService { ); user = user.toObject(); user.type = 'CoreUser'; + this.emitDtpEvent('user.login', { user }); return cb(null, user); } catch (error) { return cb(error); @@ -713,6 +794,11 @@ class CoreNodeService extends SiteService { }, ); } + + async removeUser (core, user) { + this.log.alert('remove Core user', { core: core.meta.name, user: user.username }); + await this.emitDtpEvent('user.remove', { core, user }); + } } module.exports = { diff --git a/app/services/hive.js b/app/services/hive.js index 3b3007d..b6366a7 100644 --- a/app/services/hive.js +++ b/app/services/hive.js @@ -7,6 +7,8 @@ const mongoose = require('mongoose'); const UserSubscription = mongoose.model('UserSubscription'); +const UserNotification = mongoose.model('UserSubscription'); + const KaleidoscopeEvent = mongoose.model('KaleidoscopeEvent'); const slug = require('slug'); @@ -20,6 +22,25 @@ class HiveService extends SiteService { super(dtp, module.exports); } + async start ( ) { + const { oauth2: oauth2Service } = this.dtp.services; + + this.eventHandlers = { + onOAuth2RemoveClient: this.onOAuth2RemoveClient.bind(this), + }; + + oauth2Service.on(oauth2Service.getEventName('client.remove'), this.eventHandlers.onOAuth2RemoveClient); + } + + async stop ( ) { + const { oauth2: oauth2Service } = this.dtp.services; + + oauth2Service.off(oauth2Service.getEventName('client.remove'), this.eventHandlers.onOAuth2RemoveClient); + delete this.eventHandlers.onOAuth2RemoveClient; + + delete this.eventHandlers; + } + async subscribe (user, client, emitterId) { await UserSubscription.updateOne( { user: user._id }, @@ -85,7 +106,7 @@ class HiveService extends SiteService { throw new SiteError(403, 'Unknown client domain key'); } - const event = await this.createKaleidoscopeEvent(eventDefinition); + const event = await this.createKaleidoscopeEvent(eventDefinition, client); await UserSubscription .find({ 'subscriptions.client': client._id, @@ -100,7 +121,7 @@ class HiveService extends SiteService { this.emit('kaleidoscope:event', event, client); } - async createKaleidoscopeEvent (eventDefinition) { + async createKaleidoscopeEvent (eventDefinition, sourceClient) { const NOW = new Date(); /* @@ -185,8 +206,16 @@ class HiveService extends SiteService { throw new SiteError(406, 'Missing source emitter href'); } + /* + * Create the KaleidoscopeEvent document + */ + const event = new KaleidoscopeEvent(); - event.created = NOW; + if (eventDefinition.created) { + event.created = new Date(eventDefinition.created); + } else { + event.created = NOW; + } if (eventDefinition.recipientType && eventDefinition.recipient) { event.recipientType = eventDefinition.recipientType; @@ -219,6 +248,10 @@ class HiveService extends SiteService { }, }; + if (sourceClient) { + event.source.client = sourceClient._id; + } + if (eventDefinition.source.emitter) { event.source.emitter = { emitterType: striptags(eventDefinition.source.emitter.emitterType), @@ -273,6 +306,27 @@ class HiveService extends SiteService { .lean(); return { events, totalEventCount }; } + + /* + * OAuth2 event handlers + */ + + /** + * This event fires when an OAuth2Client is being disconnected and removed by a + * Core, or a client app is being removed from a Service Node. The Hive service + * will remove all KaleidoscopeEvent records created on behalf of the client. + * @param {OAuth2Client} client the client being removed + */ + async onOAuth2RemoveClient (client) { + this.log.alert('removing KaleidoscopeEvent records from OAuth2Client', { clientId: client._id, domain: client.site.domain }); + await KaleidoscopeEvent + .find({ 'source.client': client._id }) + .cursor() + .eachAsync(async (event) => { + await UserNotification.deleteMany({ event: event._id }); + await KaleidoscopeEvent.deleteOne({ _id: event._id }); + }, 1); + } } module.exports = { diff --git a/app/services/oauth2.js b/app/services/oauth2.js index 2e886b8..4608d05 100644 --- a/app/services/oauth2.js +++ b/app/services/oauth2.js @@ -464,6 +464,10 @@ class OAuth2Service extends SiteService { * @param {OAuth2Client} client the client to be removed */ async removeClient (client) { + // provides opportunity to allow or disallow and, if allowed, perform any + // additional cleanup needed when removing a client. + await this.emitDtpEvent('client.remove', client); + this.log.info('removing client', { clientId: client._id, }); await OAuth2Client.deleteOne({ _id: client._id }); } diff --git a/app/services/user.js b/app/services/user.js index 3a5ca6b..afba7f6 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -738,33 +738,38 @@ class UserService extends SiteService { await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } }); } - async blockUser (userId, blockedUserId) { - userId = mongoose.Types.ObjectId(userId); - blockedUserId = mongoose.Types.ObjectId(blockedUserId); - if (userId.equals(blockedUserId)) { + async blockUser (user, blockedUser) { + if (user._id.equals(blockedUser._id)) { throw new SiteError(406, "You can't block yourself"); } await UserBlock.updateOne( - { user: userId }, + { 'member.user': user._id }, { - $addToSet: { blockedUsers: blockedUserId }, + $addToSet: { + blockedMembers: { + userType: blockedUser.type, + user: blockedUser._id, + }, + }, }, { upsert: true }, ); } - async unblockUser (userId, blockedUserId) { - userId = mongoose.Types.ObjectId(userId); - blockedUserId = mongoose.Types.ObjectId(blockedUserId); - if (userId.equals(blockedUserId)) { + async unblockUser (user, blockedUser) { + if (user._id.equals(blockedUser._id)) { throw new SiteError(406, "You can't un-block yourself"); } await UserBlock.updateOne( - { user: userId }, + { 'member.user': user._id }, { - $removeFromSet: { blockedUsers: blockedUserId }, + $removeFromSet: { + blockedUsers: { + userType: blockedUser.type, + user: blockedUser._id, + }, + }, }, - { upsert: true }, ); } diff --git a/app/views/admin/announcement/index.pug b/app/views/admin/announcement/index.pug index 0e421ca..3c242ba 100644 --- a/app/views/admin/announcement/index.pug +++ b/app/views/admin/announcement/index.pug @@ -5,7 +5,7 @@ block content .uk-width-expand h1 Announcements .uk-width-auto - a(href="/admin/announcement/create").uk-button.dtp-button-primary + a(href="/admin/announcement/create").uk-button.dtp-button-primary.uk-border-rounded span i.fas.fa-plus span.uk-margin-small-left Create @@ -21,7 +21,7 @@ block content i(class=`fas ${announcement.title.icon.class}`) span.uk-margin-small-left= announcement.title.content .uk-width-auto - button(type="button", data-announcement-id= announcement._id, onclick="return dtp.adminApp.deleteAnnouncement(event);").uk-button.dtp-button-danger + button(type="button", data-announcement-id= announcement._id, onclick="return dtp.adminApp.deleteAnnouncement(event);").uk-button.dtp-button-danger.uk-border-rounded span i.fas.fa-trash else diff --git a/app/views/admin/core-node/view.pug b/app/views/admin/core-node/view.pug index a728eaa..414e279 100644 --- a/app/views/admin/core-node/view.pug +++ b/app/views/admin/core-node/view.pug @@ -8,20 +8,33 @@ block content div(uk-grid).uk-grid-small.uk-flex-middle div(class="uk-width-1-1 uk-width-expand@m") h1(style="line-height: 1em;") Core Node - div(class="uk-width-1-1 uk-width-auto@m") + + .uk-width-auto a(href=`mailto:${coreNode.meta.supportEmail}?subject=${encodeURIComponent(`Support request from ${site.name}`)}`) span i.fas.fa-envelope span.uk-margin-small-left Email Support - div(class="uk-width-1-1 uk-width-auto@m") + + .uk-width-auto span.uk-label(style="line-height: 1.75em;", class={ 'uk-label-success': coreNode.flags.isConnected, 'uk-label-warning': !coreNode.flags.isConnected && !coreNode.flags.isBlocked, 'uk-label-danger': coreNode.flags.isBlocked, }).no-select= coreNode.flags.isConnected ? 'Connected' : 'Pending' + +renderCoreNodeListItem(coreNode) + .uk-margin + button( + type="button", + data-core={ _id: coreNode._id, name: coreNode.meta.name}, + onclick="return dtp.adminApp.disconnectCore(event);", + ).uk-button.dtp-button-danger.uk-border-rounded + span + i.fas.fa-window-close + span.uk-margin-small-left Disconnect + .uk-margin table.uk-table.uk-table-small thead diff --git a/app/views/admin/newsletter/index.pug b/app/views/admin/newsletter/index.pug index fa0f0a2..f3874df 100644 --- a/app/views/admin/newsletter/index.pug +++ b/app/views/admin/newsletter/index.pug @@ -21,7 +21,7 @@ block content data-newsletter-id= newsletter._id, data-newsletter-title= newsletter.title, onclick="return dtp.adminApp.deleteNewsletter(event);", - ).uk-button.uk-button-danger + ).uk-button.uk-button-danger.uk-border-rounded +renderButtonIcon('fa-trash', 'Delete') .uk-width-auto @@ -30,7 +30,7 @@ block content data-newsletter-id= newsletter._id, data-newsletter-title= newsletter.title, onclick="return dtp.adminApp.sendNewsletter(event);", - ).uk-button.uk-button-default + ).uk-button.uk-button-default.uk-border-rounded +renderButtonIcon('fa-paper-plane', 'Send') else div There are no newsletters at this time. \ No newline at end of file diff --git a/app/views/admin/newsletter/job-status.pug b/app/views/admin/newsletter/job-status.pug index 6ea723c..7d7ac4e 100644 --- a/app/views/admin/newsletter/job-status.pug +++ b/app/views/admin/newsletter/job-status.pug @@ -6,40 +6,59 @@ block content .uk-margin h1 Job Queue: #{queueName} div(uk-grid).uk-flex-between - - var pendingJobCount = jobCounts.waiting + jobCounts.delayed + jobCounts.paused + jobCounts.active - .uk-width-auto Total#[br]#{numeral(pendingJobCount).format('0,0')} - .uk-width-auto Waiting#[br]#{numeral(jobCounts.waiting).format('0,0')} - .uk-width-auto Delayed#[br]#{numeral(jobCounts.delayed).format('0,0')} - .uk-width-auto Paused#[br]#{numeral(jobCounts.paused).format('0,0')} - .uk-width-auto Active#[br]#{numeral(jobCounts.active).format('0,0')} - .uk-width-auto Completed#[br]#{numeral(jobCounts.completed).format('0,0')} - .uk-width-auto Failed#[br]#{numeral(jobCounts.failed).format('0,0')} + .uk-width-auto + label.uk-form-label.uk-text-primary Active + .uk-text-large= numeral(jobCounts.active).format('0,0') + .uk-width-auto + label.uk-form-label.uk-text-success Completed + .uk-text-large= numeral(jobCounts.completed).format('0,0') + .uk-width-auto + label.uk-form-label.uk-text-warning Delayed + .uk-text-large= numeral(jobCounts.delayed).format('0,0') + .uk-width-auto + label.uk-form-label.uk-text-danger Failed + .uk-text-large= numeral(jobCounts.failed).format('0,0') + .uk-width-auto + label.uk-form-label.uk-text-muted Waiting + .uk-text-large= numeral(jobCounts.waiting).format('0,0') + .uk-width-auto + label.uk-form-label.uk-text-muted Paused + .uk-text-large= numeral(jobCounts.paused).format('0,0') div(uk-grid) div(class="uk-width-1-1 uk-width-1-2@l") - .uk-card.uk-card-default.uk-card-small - .uk-card-header - h3.uk-card-title Active - .uk-card-body - +renderJobQueueJobList(newsletterQueue, jobs.active) + .uk-margin + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h3.uk-card-title Active + .uk-card-body + +renderJobQueueJobList(newsletterQueue, jobs.active) - div(class="uk-width-1-1 uk-width-1-2@l") - .uk-card.uk-card-default.uk-card-small - .uk-card-header - h3.uk-card-title Waiting - .uk-card-body - +renderJobQueueJobList(newsletterQueue, jobs.waiting) + .uk-margin + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h3.uk-card-title Delayed + .uk-card-body + +renderJobQueueJobList(newsletterQueue, jobs.delayed) - div(class="uk-width-1-1 uk-width-1-2@l") - .uk-card.uk-card-default.uk-card-small - .uk-card-header - h3.uk-card-title Delayed - .uk-card-body - +renderJobQueueJobList(newsletterQueue, jobs.delayed) + .uk-margin + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h3.uk-card-title Paused + .uk-card-body + +renderJobQueueJobList(newsletterQueue, jobs.paused) div(class="uk-width-1-1 uk-width-1-2@l") - .uk-card.uk-card-default.uk-card-small - .uk-card-header - h3.uk-card-title Failed - .uk-card-body - +renderJobQueueJobList(newsletterQueue, jobs.failed) \ No newline at end of file + .uk-margin + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h3.uk-card-title Waiting + .uk-card-body + +renderJobQueueJobList(newsletterQueue, jobs.waiting) + + .uk-margin + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h3.uk-card-title Failed + .uk-card-body + +renderJobQueueJobList(newsletterQueue, jobs.failed) \ No newline at end of file diff --git a/app/views/admin/newsroom/editor.pug b/app/views/admin/newsroom/editor.pug index 55c5eb2..684077b 100644 --- a/app/views/admin/newsroom/editor.pug +++ b/app/views/admin/newsroom/editor.pug @@ -34,6 +34,9 @@ block content .uk-card-footer div(uk-grid).uk-flex-right.uk-flex-middle + .uk-width-expand + +renderBackButton() + if feed .uk-width-auto button( @@ -41,6 +44,13 @@ block content data-feed-id= feed._id, data-feed-title= feed.title, onclick="return dtp.adminApp.removeNewsroomFeed(event);", - ).uk-button.uk-button-danger.uk-border-rounded Remove Feed + ).uk-button.uk-button-danger.uk-border-rounded + span + i.fas.fa-trash + span.uk-margin-small-left Remove Feed + .uk-width-auto - button(type="submit").uk-button.uk-button-primary.uk-border-rounded= feed ? 'Update Feed' : 'Add Feed' \ No newline at end of file + button(type="submit").uk-button.uk-button-primary.uk-border-rounded + span + i.fas.fa-save + span.uk-margin-small-left= feed ? 'Update Feed' : 'Add Feed' \ No newline at end of file diff --git a/app/views/admin/settings/editor.pug b/app/views/admin/settings/editor.pug index ec8aab5..725c770 100644 --- a/app/views/admin/settings/editor.pug +++ b/app/views/admin/settings/editor.pug @@ -28,16 +28,6 @@ block content legend Featured Embed textarea(id="featured-embed", name="featuredEmbed", rows="4").uk-textarea.uk-resize-vertical= site.featuredEmbed - fieldset - legend Shing.tv Widget Key - div(uk-grid).uk-grid-small - div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl") - label(for="shing-channel-slug").uk-form-label Shing.tv Channel Slug - input(id="shing-channel-slug", name="shingChannelSlug", type="text", placeholder="Enter Shing.tv channel slug", value= site.shingChannelSlug).uk-input - div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl") - label(for="shing-widget-key").uk-form-label Shing.tv Widget Key - input(id="shing-widget-key", name="shingWidgetKey", type="text", placeholder="Enter Shing.tv widget key", value= site.shingWidgetKey).uk-input - fieldset legend Gab links div(uk-grid).uk-grid-small @@ -95,4 +85,4 @@ block content label(for="spreaker-url").uk-form-label Spreaker URL input(id="spreaker-url", name="spreakerUrl", type="url", placeholder="Enter Spreaker URL", value= site.spreakerUrl).uk-input - button(type="submit").uk-button.dtp-button-primary Save Settings \ No newline at end of file + button(type="submit").uk-button.dtp-button-primary.uk-border-rounded Save Settings \ No newline at end of file diff --git a/app/views/components/library.pug b/app/views/components/library.pug index 338e4f9..0aa6d3f 100644 --- a/app/views/components/library.pug +++ b/app/views/components/library.pug @@ -52,7 +52,7 @@ mixin renderCell (label, value, className) mixin renderBackButton (options) - options = Object.assign({ includeLabel: true, label: 'Back' }, options) - button(type="button", onclick="window.history.back();").uk-button.uk-button-default + button(type="button", onclick="window.history.back();").uk-button.uk-button-default.uk-border-rounded span i.fas.fa-chevron-left if options.includeLabel diff --git a/app/views/welcome/index.pug b/app/views/welcome/index.pug index 5dc3183..0db601a 100644 --- a/app/views/welcome/index.pug +++ b/app/views/welcome/index.pug @@ -13,13 +13,24 @@ block content div(uk-grid).uk-flex-center div(class="uk-width-1-1 uk-width-1-3@m") .uk-margin-small - a(href="/auth/core").uk-button.uk-button-primary.uk-border-rounded DTP Connect + a(href="/auth/core").uk-button.uk-button-primary.uk-border-rounded + span + i.fas.fa-plug + span.uk-margin-small-left DTP Connect .uk-text-small Connect using DTP Core + div(class="uk-width-1-1 uk-width-1-3@m") .uk-margin-small - a(href="/welcome/signup").uk-button.uk-button-secondary.uk-border-rounded Create Account + a(href="/welcome/signup").uk-button.uk-button-secondary.uk-border-rounded + span + i.fas.fa-user-plus + span.uk-margin-small-left Create Account .uk-text-small Create a local account + div(class="uk-width-1-1 uk-width-1-3@m") .uk-margin-small - a(href="/welcome/login").uk-button.uk-button-default.uk-border-rounded Sign In + a(href="/welcome/login").uk-button.uk-button-default.uk-border-rounded + span + i.fas.fa-door-open + span.uk-margin-small-left Sign In .uk-text-small Log in with your local account \ No newline at end of file diff --git a/app/views/welcome/login.pug b/app/views/welcome/login.pug index 40b2a25..49b4cfa 100644 --- a/app/views/welcome/login.pug +++ b/app/views/welcome/login.pug @@ -32,4 +32,4 @@ block content .uk-width-auto a(href="/").uk-text-muted Forgot password .uk-width-auto - button(type="submit").uk-button.dtp-button-primary Login \ No newline at end of file + button(type="submit").uk-button.dtp-button-primary.uk-border-rounded Login \ No newline at end of file diff --git a/app/views/welcome/signup.pug b/app/views/welcome/signup.pug index 48396ed..6e0ccc2 100644 --- a/app/views/welcome/signup.pug +++ b/app/views/welcome/signup.pug @@ -56,4 +56,4 @@ block content .uk-width-expand +renderBackButton() .uk-width-auto - button(type="submit").uk-button.uk-button-primary Create Account + button(type="submit").uk-button.uk-button-primary.uk-border-rounded Create Account diff --git a/app/workers/media/job/sticker-ingest.js b/app/workers/media/job/sticker-ingest.js index c4f6fcb..599f8c1 100644 --- a/app/workers/media/job/sticker-ingest.js +++ b/app/workers/media/job/sticker-ingest.js @@ -128,7 +128,7 @@ class StickerIngestJob extends SiteWorkerProcess { throw new Error(`unsupported sticker type: ${job.data.sticker.original.type}`); } - this.jobLog(job, 'fetching original media', { // blah 62096adcb69874552a0f87bc + this.jobLog(job, 'fetching original media', { stickerId: job.data.sticker._id, slug: job.data.sticker.slug, type: job.data.sticker.original.type, diff --git a/client/js/site-admin-app.js b/client/js/site-admin-app.js index 5339f9c..ff6bb1f 100644 --- a/client/js/site-admin-app.js +++ b/client/js/site-admin-app.js @@ -404,7 +404,6 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { try { await UIkit.modal.confirm(`Are you sure you want to remove post "${postTitle}"?`); } catch (error) { - // canceled return false; } @@ -429,7 +428,6 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { try { await UIkit.modal.confirm(`Are you sure you want to remove page "${pageTitle}"?`); } catch (error) { - // canceled return false; } @@ -453,7 +451,6 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { try { await UIkit.modal.confirm(`Are you sure you want to remove site link "${link.label}"?`); } catch (error) { - // canceled return false; } @@ -477,7 +474,6 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { try { await UIkit.modal.confirm(`Are you sure you want to remove channel "${channel.name}"?`); } catch (error) { - // canceled return false; } @@ -487,6 +483,27 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { } catch (error) { UIkit.modal.alert(`Failed to remove site link: ${error.message}`); } + return false; + } + + async disconnectCore (event) { + event.preventDefault(); + event.stopPropagation(); + const target = event.currentTarget || event.target; + const core = JSON.parse(target.getAttribute('data-core')); + + try { + await UIkit.modal.confirm(`Are you sure you want to disconnect from Core "${core.name}"?`); + } catch (error) { + return false; + } + + try { + const response = await fetch(`/admin/core-node/${core._id}`, { method: 'DELETE' }); + await this.processResponse(response); + } catch (error) { + UIkit.modal.alert(`Failed to disconnect from Core: ${error.message}`); + } return false; } diff --git a/client/less/site/button.less b/client/less/site/button.less index 27bbe18..f63fcaf 100644 --- a/client/less/site/button.less +++ b/client/less/site/button.less @@ -97,7 +97,9 @@ button.uk-button.dtp-button-subscribe { } a.uk-button.dtp-button-default, -button.uk-button.dtp-button-default { +a.uk-button.uk-button-default, +button.uk-button.dtp-button-default, +button.uk-button.uk-button-default { background: none; outline: none; border: solid 2px rgb(75, 75, 75); @@ -107,47 +109,54 @@ button.uk-button.dtp-button-default { &:hover { background-color: rgb(75, 75, 75); + color: #e8e8e8; } } a.uk-button.dtp-button-primary, -button.uk-button.dtp-button-primary { +a.uk-button.uk-button-primary, +button.uk-button.dtp-button-primary, +button.uk-button.uk-button-primary { background: none; outline: none; border: solid 2px #1e87f0; - color: #c8c8c8; + color: @button-label-color; transition: background-color 0.2s; &:hover { background-color: #1e87f0; + color: #e8e8e8; } } - - a.uk-button.dtp-button-secondary, -button.uk-button.dtp-button-secondary { +a.uk-button.uk-button-secondary, +button.uk-button.dtp-button-secondary, +button.uk-button.uk-button-secondary { background: none; outline: none; - border: solid 2px rgb(75, 75, 75); - color: #c8c8c8; + border: solid 2px rgb(160,160,160); + color: @button-label-color; &:hover { - background-color: rgb(75, 75, 75); + background-color: rgb(160,160,160); + color: #e8e8e8; } } a.uk-button.dtp-button-danger, -button.uk-button.dtp-button-danger { +a.uk-button.uk-button-danger, +button.uk-button.dtp-button-danger, +button.uk-button.uk-button-danger { background: none; outline: none; border: solid 2px rgb(255, 0, 0); - color: @global-color; + color: @button-label-color; &:hover { background-color: rgb(255, 0, 0); - color: #ffffff; + color: #e8e8e8; } } diff --git a/lib/site-common.js b/lib/site-common.js index 8c00f31..78a1788 100644 --- a/lib/site-common.js +++ b/lib/site-common.js @@ -12,11 +12,29 @@ const striptags = require('striptags'); const { SiteLog } = require(path.join(__dirname, 'site-log')); const { SiteAsync } = require(path.join(__dirname, 'site-async')); -const Events = require('events'); -class SiteCommon extends Events { +const EventEmitter2 = require('eventemitter2'); +class SiteCommon extends EventEmitter2 { + + constructor (dtp, component, options) { + // ensure valid options + options = Object.assign({ }, options); + + // provide DTP's default EventEmitter2 configuration + options.emitter = Object.assign({ + wildcard: false, + delimiter: '.', + newListener: false, + removeListener: false, + maxListeners: 64, + verboseMemoryLeak: process.env.NODE_ENV === 'local', + ignoreErrors: false, + }, options); + + // construct the EventEmitter2 instance via super() + super(options.emitter); - constructor (dtp, component) { - super(); + // store options *after* super() because you can't alter `this` prior + this.options = options; this.dtp = dtp; this.component = component; @@ -43,6 +61,14 @@ class SiteCommon extends Events { }, 1); } + getEventName (name) { + return `dtp.${this.component.slug}.${name}`; + } + + async emitDtpEvent (name, params) { + await this.emitAsync(this.getEventName(name), params); + } + async getJobQueue (name) { if (this.jobQueues[name]) { return this.jobQueues[name]; diff --git a/lib/site-platform.js b/lib/site-platform.js index 420a29e..3b390d0 100644 --- a/lib/site-platform.js +++ b/lib/site-platform.js @@ -184,6 +184,12 @@ module.exports.startPlatform = async (dtp) => { try { module.log = new SiteLog(module, dtp.config.component); + + if (process.env.NODE_ENV === 'local') { + module.log.alert('allowing self-signed certificates for host-to-host communications', { env: process.env.NODE_ENV }); + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + } + dtp.config.jobQueues = require(path.join(dtp.config.root, 'config', 'job-queues')); await module.connectDatabase(dtp); diff --git a/package.json b/package.json index 94f2281..86053dc 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,11 @@ "diskusage-ng": "^1.0.2", "disposable-email-provider-domains": "^1.0.9", "dotenv": "^16.0.0", - "dtp-jshint-reporter": "ssh://git@git.digitaltelepresence.com:digital-telepresence/dtp-jshint-reporter.git#master", + "dtp-jshint-reporter": "git+https://git.digitaltelepresence.com/digital-telepresence/dtp-jshint-reporter.git#master", "ein-validator": "^1.0.1", "email-domain-check": "^1.1.4", "email-validator": "^2.0.4", + "eventemitter2": "^6.4.9", "execa": "^6.1.0", "express": "^4.17.3", "express-limiter": "^1.6.1", diff --git a/yarn.lock b/yarn.lock index 3f558f5..8582465 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3146,9 +3146,9 @@ drange@^1.0.2: resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8" integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA== -"dtp-jshint-reporter@ssh://git@git.digitaltelepresence.com:digital-telepresence/dtp-jshint-reporter.git#master": - version "1.0.2" - resolved "ssh://git@git.digitaltelepresence.com:digital-telepresence/dtp-jshint-reporter.git#68b078b75cd6d048a9bf9bdc9b30ccc2a2145c4f" +"dtp-jshint-reporter@git+https://git.digitaltelepresence.com/digital-telepresence/dtp-jshint-reporter.git#master": + version "1.0.4" + resolved "git+https://git.digitaltelepresence.com/digital-telepresence/dtp-jshint-reporter.git#517c2f8055140b89cd3bbfff1cdf33669b416322" dependencies: chalk "^4.1.1" @@ -3525,6 +3525,11 @@ etag@1.8.1, etag@^1.8.1, etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eventemitter2@^6.4.9: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"