diff --git a/.env.default b/.env.default index 2516829..f472529 100644 --- a/.env.default +++ b/.env.default @@ -84,6 +84,8 @@ MINIO_PORT=9000 MINIO_USE_SSL=disabled MINIO_ACCESS_KEY=dtp-sites MINIO_SECRET_KEY= + +MINIO_ADMIN_BUCKET=site-admin MINIO_IMAGE_BUCKET=site-images MINIO_VIDEO_BUCKET=site-videos MINIO_ATTACHMENT_BUCKET=site-attachments @@ -112,4 +114,14 @@ DTP_LOG_DEBUG=enabled DTP_LOG_INFO=enabled DTP_LOG_WARN=enabled -DTP_LOG_HTTP_FORMAT=combined \ No newline at end of file +DTP_LOG_HTTP_FORMAT=combined + +# +# DTP Logan Integration +# + +DTP_LOGAN=disabled +DTP_LOGAN_API_KEY=########-####-####-####-############ +DTP_LOGAN_SCHEME=https +DTP_LOGAN_HOST=logan.digitaltelepresence.com +DTP_LOGAN_QUEUE_NAME=logan \ No newline at end of file diff --git a/.jshintrc b/.jshintrc index 6dd1a7e..c3942c1 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,7 +10,7 @@ "undef": true, "unused": true, "futurehostile": true, - "esversion": 9, + "esversion": 11, "mocha": true, "globals": { "markdown": true, diff --git a/.vscode/launch.json b/.vscode/launch.json index 6166589..68255b8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,6 +30,27 @@ "console": "integratedTerminal", "args": ["--action=reset-indexes", "all"] }, + { + "type": "node", + "request": "launch", + "name": "worker:newsletter", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder:dtp-base}/app/workers/newsletter.js", + "console": "integratedTerminal", + }, + { + "type": "node", + "request": "launch", + "name": "worker:newsroom", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder:dtp-base}/app/workers/newsroom.js", + "console": "integratedTerminal", + }, + { "type": "node", "request": "launch", diff --git a/app/controllers/admin.js b/app/controllers/admin.js index 3a63eec..b193f32 100644 --- a/app/controllers/admin.js +++ b/app/controllers/admin.js @@ -43,10 +43,12 @@ class AdminController extends SiteController { ); router.use('/announcement', await this.loadChild(path.join(__dirname, 'admin', 'announcement'))); + router.use('/attachment', await this.loadChild(path.join(__dirname, 'admin', 'attachment'))); router.use('/content-report', await this.loadChild(path.join(__dirname, 'admin', 'content-report'))); router.use('/core-node', await this.loadChild(path.join(__dirname, 'admin', 'core-node'))); router.use('/core-user', await this.loadChild(path.join(__dirname, 'admin', 'core-user'))); router.use('/host', await this.loadChild(path.join(__dirname, 'admin', 'host'))); + router.use('/image', await this.loadChild(path.join(__dirname, 'admin', 'image'))); router.use('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue'))); router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log'))); router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter'))); @@ -76,35 +78,53 @@ class AdminController extends SiteController { }); } - async getHomeView (req, res) { + async getHomeView (req, res, next) { const { + chat: chatService, + comment: commentService, coreNode: coreNodeService, dashboard: dashboardService, venue: venueService, logan: loganService, + user: userService, } = this.dtp.services; - - res.locals.stats = { - userSignupHourly: await dashboardService.getUserSignupsPerHour(), - memberCount: await User.estimatedDocumentCount(), - constellation: await coreNodeService.getConstellationStats(), - }; - - res.locals.channels = await venueService.getChannels(); - res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`; - - loganService.sendRequestEvent(module.exports, req, { - level: 'info', - event: 'getHomeView', - }); - - res.render('admin/index'); + try { + res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`; + res.locals.stats = { + userSignupHourly: await dashboardService.getUserSignupsPerHour(), + memberCount: await User.estimatedDocumentCount(), + constellation: await coreNodeService.getConstellationStats(), + }; + + try { + res.locals.channels = await venueService.getChannels(); + } catch (error) { + // fall through + res.locals.channels = [ ]; + } + + res.locals.recentMembers = await userService.getRecent(10); + res.locals.admins = await userService.getAdmins(); + res.locals.moderators = await userService.getModerators(); + + res.locals.recentComments = await commentService.getRecent(10); + res.locals.recentChat = await chatService.getRecent(10); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getHomeView', + }); + + res.render('admin/index'); + } catch (error) { + return next(error); + } } } module.exports = { - slug: 'admin', - name: 'admin', + logId: 'ctl:admin', + index: 'admin', className: 'AdminController', create: async (dtp) => { return new AdminController(dtp); }, }; diff --git a/app/controllers/admin/announcement.js b/app/controllers/admin/announcement.js index 5edb1d3..30bee40 100644 --- a/app/controllers/admin/announcement.js +++ b/app/controllers/admin/announcement.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController } = require('../../../lib/site-lib'); -class AnnouncementAdminController extends SiteController { +class AdminAnnouncementController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -158,8 +158,8 @@ class AnnouncementAdminController extends SiteController { } module.exports = { - name: 'announcement', - slug: 'announcement', - className: 'AnnouncementAdminController', - create: async (dtp) => { return new AnnouncementAdminController(dtp); }, + logId: 'ctl:admin:announcement', + index: 'adminAnnouncement', + className: 'AdminAnnouncementController', + create: async (dtp) => { return new AdminAnnouncementController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/attachment.js b/app/controllers/admin/attachment.js new file mode 100644 index 0000000..f1c621a --- /dev/null +++ b/app/controllers/admin/attachment.js @@ -0,0 +1,137 @@ +// admin/attachment.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const express = require('express'); + +const { SiteController } = require('../../../lib/site-lib'); + +class AdminAttachmentController extends SiteController { + + constructor (dtp) { + super(dtp, module.exports); + } + + async start ( ) { + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'attachment'; + return next(); + }); + + router.param('attachmentId', this.populateAttachmentId.bind(this)); + + router.post('/:attachmentId', this.postUpdateAttachment.bind(this)); + + router.get('/:attachmentId', this.getAttachmentView.bind(this)); + + router.get('/', this.getDashboard.bind(this)); + + router.delete('/:attachmentId', this.deleteAttachment.bind(this)); + + return router; + } + + async populateAttachmentId (req, res, next, attachmentId) { + const { + attachment: attachmentService, + logan: loganService, + } = this.dtp.services; + try { + res.locals.attachment = await attachmentService.getById(attachmentId); + return next(); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populateAttachmentId', + message: `failed to populate attachment: ${error.message}`, + data: { attachmentId, error }, + }); + return next(error); + } + } + + async postUpdateAttachment (req, res, next) { + const { + attachment: attachmentService, + logan: loganService, + } = this.dtp.services; + try { + await attachmentService.update(res.locals.attachment, req.body); + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postUpdateAttachment', + data: { + attachment: { + _id: res.locals.attachment._id, + }, + }, + }); + res.redirect('/admin/attachment'); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postUpdateAttachment', + message: `failed to update attachment: ${error.message}`, + data: { error }, + }); + return next(error); + } + } + + async getAttachmentView (req, res) { + res.render('admin/attachment/view'); + } + + async getDashboard (req, res, next) { + const { attachment: attachmentService } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.attachments = await attachmentService.getRecent(res.locals.pagination); + res.render('admin/attachment/index'); + } catch (error) { + return next(error); + } + } + + async deleteAttachment (req, res) { + const { + attachment: attachmentService, + logan: loganService, + } = this.dtp.services; + try { + const displayList = this.createDisplayList('delete-attachment'); + await attachmentService.remove(res.locals.attachment); + displayList.reload(); + + res.status(200).json({ success: true, displayList }); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'deleteAttachment', + data: { attachment: { _id: res.locals.attachment._id } }, + }); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'deleteAttachment', + message: `failed to delete attachment: ${error.message}`, + data: { error }, + }); + res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } +} + +module.exports = { + logId: 'ctl:admin:attachment', + index: 'adminAttachment', + className: 'AdminAttachmentController', + create: async (dtp) => { return new AdminAttachmentController(dtp); }, +}; \ No newline at end of file diff --git a/app/controllers/admin/content-report.js b/app/controllers/admin/content-report.js index e7d0f2f..65af3aa 100644 --- a/app/controllers/admin/content-report.js +++ b/app/controllers/admin/content-report.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController } = require('../../../lib/site-lib'); -class ContentReportAdminController extends SiteController { +class AdminContentReportController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -87,8 +87,8 @@ class ContentReportAdminController extends SiteController { } module.exports = { - name: 'adminContentReport', - slug: 'admin-content-report', - className: 'ContentReportAdminController', - create: async (dtp) => { return new ContentReportAdminController(dtp); }, + logId: 'ctl:admin:content-report', + index: 'adminContentReport', + className: 'AdminContentReportController', + create: async (dtp) => { return new AdminContentReportController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/core-node.js b/app/controllers/admin/core-node.js index a9e053e..f311c36 100644 --- a/app/controllers/admin/core-node.js +++ b/app/controllers/admin/core-node.js @@ -9,7 +9,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class CoreNodeAdminController extends SiteController { +class AdminCoreNodeController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -140,8 +140,8 @@ class CoreNodeAdminController extends SiteController { } module.exports = { - name: 'adminCoreNode', - slug: 'admin-core-node', - className: 'CoreNodeAdminController', - create: async (dtp) => { return new CoreNodeAdminController(dtp); }, + logId: 'ctl:admin:core-node', + index: 'adminCoreNode', + className: 'AdminCoreNodeController', + create: async (dtp) => { return new AdminCoreNodeController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/core-user.js b/app/controllers/admin/core-user.js index 28d0366..6984833 100644 --- a/app/controllers/admin/core-user.js +++ b/app/controllers/admin/core-user.js @@ -9,7 +9,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class CoreUserAdminController extends SiteController { +class AdminCoreUserController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -88,8 +88,8 @@ class CoreUserAdminController extends SiteController { } module.exports = { - name: 'adminCoreUser', - slug: 'admin-core-user', - className: 'CoreUserAdminController', - create: async (dtp) => { return new CoreUserAdminController(dtp); }, + logId: 'ctl:admin:core-user', + index: 'adminCoreUser', + className: 'AdminCoreUserController', + create: async (dtp) => { return new AdminCoreUserController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/host.js b/app/controllers/admin/host.js index 979a6d5..aff860d 100644 --- a/app/controllers/admin/host.js +++ b/app/controllers/admin/host.js @@ -12,7 +12,7 @@ const NetHostStats = mongoose.model('NetHostStats'); const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib'); -class HostAdminController extends SiteController { +class AdminHostController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -116,8 +116,8 @@ class HostAdminController extends SiteController { } module.exports = { - name: 'adminHost', - slug: 'admin-host', - className: 'HostAdminController', - create: async (dtp) => { return new HostAdminController(dtp); }, + logId: 'ctl:admin:host', + index: 'adminHost', + className: 'AdminHostController', + create: async (dtp) => { return new AdminHostController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/image.js b/app/controllers/admin/image.js new file mode 100644 index 0000000..7bab8c9 --- /dev/null +++ b/app/controllers/admin/image.js @@ -0,0 +1,121 @@ +// admin/image.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const express = require('express'); + +const { SiteController, SiteError } = require('../../../lib/site-lib'); + +class AdminImageController extends SiteController { + + constructor (dtp) { + super(dtp, module.exports); + } + + async start ( ) { + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'image'; + return next(); + }); + + router.param('imageId', this.populateImageId.bind(this)); + + router.get('/:imageId/archive-user', this.getUserArchiveView.bind(this)); + router.get('/:imageId', this.getImageView.bind(this)); + + router.get('/', this.getDashboard.bind(this)); + + router.delete('/:imageId', this.deleteImage.bind(this)); + + return router; + } + + async populateImageId (req, res, next, imageId) { + const { + image: imageService, + logan: loganService, + } = this.dtp.services; + try { + res.locals.image = await imageService.getImageById(imageId); + if (!res.locals.image) { + throw new SiteError(404, 'Image not found'); + } + return next(); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populateImageId', + message: `failed to populate image: ${error.message}`, + data: { imageId, error }, + }); + return next(error); + } + } + + async getUserArchiveView (req, res, next) { + const { image: imageService } = this.dtp.services; + try { + res.locals.imageHistory = await imageService.getRecentImagesForOwner(res.locals.image.owner, 10); + res.render('admin/image/archive-user'); + } catch (error) { + return next(error); + } + } + + async getImageView (req, res) { + res.render('admin/image/view'); + } + + async getDashboard (req, res, next) { + const { image: imageService } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.images = await imageService.getRecentImages(res.locals.pagination); + res.render('admin/image/index'); + } catch (error) { + return next(error); + } + } + + async deleteImage (req, res) { + const { + image: imageService, + logan: loganService, + } = this.dtp.services; + try { + const displayList = this.createDisplayList('delete-image'); + await imageService.deleteImage(res.locals.image); + displayList.reload(); + + res.status(200).json({ success: true, displayList }); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'deleteImage', + data: { image: { _id: res.locals.image._id } }, + }); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'deleteImage', + message: `failed to delete image: ${error.message}`, + data: { error }, + }); + res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } +} + +module.exports = { + logId: 'ctl:admin:image', + index: 'adminImage', + className: 'AdminImageController', + create: async (dtp) => { return new AdminImageController(dtp); }, +}; \ No newline at end of file diff --git a/app/controllers/admin/job-queue.js b/app/controllers/admin/job-queue.js index 9f16c47..7e3eda3 100644 --- a/app/controllers/admin/job-queue.js +++ b/app/controllers/admin/job-queue.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class JobQueueAdminController extends SiteController { +class AdminJobQueueController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -119,8 +119,8 @@ class JobQueueAdminController extends SiteController { } module.exports = { - name: 'adminJobQueue', - slug: 'admin-job-queue', - className: 'JobQueueAdminController', - create: async (dtp) => { return new JobQueueAdminController(dtp); }, + logId: 'ctl:admin:job-queue', + index: 'adminJobQueue', + className: 'AdminJobQueueController', + create: async (dtp) => { return new AdminJobQueueController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/log.js b/app/controllers/admin/log.js index a29a82e..8648cdb 100644 --- a/app/controllers/admin/log.js +++ b/app/controllers/admin/log.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController } = require('../../../lib/site-lib'); -class LogAdminController extends SiteController { +class AdminLogController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -32,12 +32,12 @@ class LogAdminController extends SiteController { try { res.locals.query = req.query; - res.locals.components = await logService.getComponentSlugs(); + res.locals.components = await logService.getComponentIds(); res.locals.pagination = this.getPaginationParameters(req, 25); const search = { }; if (req.query.component) { - search.component = { slug: req.query.component }; + search.component = { logId: req.query.component }; } res.locals.logs = await logService.getRecords(search, res.locals.pagination); @@ -51,8 +51,8 @@ class LogAdminController extends SiteController { } module.exports = { - name: 'adminLog', - slug: 'admin-log', - className: 'LogAdminController', - create: async (dtp) => { return new LogAdminController(dtp); }, + logId: 'ctl:admin:log', + index: 'adminLog', + className: 'AdminLogController', + create: async (dtp) => { return new AdminLogController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/newsletter.js b/app/controllers/admin/newsletter.js index a875661..e8a9152 100644 --- a/app/controllers/admin/newsletter.js +++ b/app/controllers/admin/newsletter.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class NewsletterAdminController extends SiteController { +class AdminNewsletterController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -167,8 +167,8 @@ class NewsletterAdminController extends SiteController { } module.exports = { - name: 'adminNewsletter', - slug: 'admin-newsletter', - className: 'NewsletterAdminController', - create: async (dtp) => { return new NewsletterAdminController(dtp); }, + logId: 'ctl:admin:newsletter', + index: 'adminNewsletter', + className: 'AdminNewsletterController', + create: async (dtp) => { return new AdminNewsletterController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/newsroom.js b/app/controllers/admin/newsroom.js index d014460..dfa7909 100644 --- a/app/controllers/admin/newsroom.js +++ b/app/controllers/admin/newsroom.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class NewsroomAdminController extends SiteController { +class AdminNewsroomController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -158,8 +158,8 @@ class NewsroomAdminController extends SiteController { } module.exports = { - name: 'newsroomAdmin', - slug: 'newsroom-admin', - className: 'NewsroomAdminController', - create: async (dtp) => { return new NewsroomAdminController(dtp); }, + logId: 'ctl:admin:newsroom', + index: 'adminNewsroomAdmin', + className: 'AdminNewsroomController', + create: async (dtp) => { return new AdminNewsroomController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/otp.js b/app/controllers/admin/otp.js index 9bdbd15..139b903 100644 --- a/app/controllers/admin/otp.js +++ b/app/controllers/admin/otp.js @@ -9,14 +9,14 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class OtpAdminController extends SiteController { +class AdminOtpController extends SiteController { constructor (dtp) { super(dtp, module.exports); } async start ( ) { - // const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${module.exports.slug}` }); + // const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${this.component.logId}` }); const router = express.Router(); router.use(async (req, res, next) => { @@ -49,8 +49,8 @@ class OtpAdminController extends SiteController { } module.exports = { - name: 'adminOtp', - slug: 'admin-opt', - className: 'OtpAdminController', - create: async (dtp) => { return new OtpAdminController(dtp); }, + logId: 'ctl:admin:otp', + index: 'adminOtp', + className: 'AdminOtpController', + create: async (dtp) => { return new AdminOtpController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/page.js b/app/controllers/admin/page.js index 0be4791..bb9ce15 100644 --- a/app/controllers/admin/page.js +++ b/app/controllers/admin/page.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class PageController extends SiteController { +class AdminPageController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -131,8 +131,8 @@ class PageController extends SiteController { } module.exports = { - name: 'adminPage', - slug: 'admin-page', - className: 'PageController', - create: async (dtp) => { return new PageController(dtp); }, + logId: 'ctl:admin:page', + index: 'adminPage', + className: 'AdminPageController', + create: async (dtp) => { return new AdminPageController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/post.js b/app/controllers/admin/post.js index b737b2c..1fd9a18 100644 --- a/app/controllers/admin/post.js +++ b/app/controllers/admin/post.js @@ -9,7 +9,7 @@ const multer = require('multer'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class PostController extends SiteController { +class AdminPostController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -140,8 +140,8 @@ class PostController extends SiteController { } module.exports = { - name: 'adminPost', - slug: 'admin-post', - className: 'PostController', - create: async (dtp) => { return new PostController(dtp); }, + logId: 'ctl:admin:post', + index: 'adminPost', + className: 'AdminPostController', + create: async (dtp) => { return new AdminPostController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/service-node.js b/app/controllers/admin/service-node.js index 5519b44..2958ac3 100644 --- a/app/controllers/admin/service-node.js +++ b/app/controllers/admin/service-node.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class ServiceNodeAdminController extends SiteController { +class AdminServiceNodeController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -128,8 +128,8 @@ class ServiceNodeAdminController extends SiteController { } module.exports = { - name: 'adminServiceNode', - slug: 'admin-service-node', - className: 'ServiceNodeAdminController', - create: async (dtp) => { return new ServiceNodeAdminController(dtp); }, + logId: 'ctl:admin:service-node', + index: 'adminServiceNode', + className: 'AdminServiceNodeController', + create: async (dtp) => { return new AdminServiceNodeController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/settings.js b/app/controllers/admin/settings.js index b34b2cc..a0fb106 100644 --- a/app/controllers/admin/settings.js +++ b/app/controllers/admin/settings.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController } = require('../../../lib/site-lib'); -class SettingsAdminController extends SiteController { +class AdminSettingsController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -121,12 +121,11 @@ class SettingsAdminController extends SiteController { }); } } - } module.exports = { - name: 'adminSettings', - slug: 'admin-settings', - className: 'SettingsAdminController', - create: async (dtp) => { return new SettingsAdminController(dtp); }, + logId: 'ctl:admin:settings', + index: 'adminSettings', + className: 'AdminSettingsController', + create: async (dtp) => { return new AdminSettingsController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/site-link.js b/app/controllers/admin/site-link.js index d9b4b07..30db296 100644 --- a/app/controllers/admin/site-link.js +++ b/app/controllers/admin/site-link.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController } = require('../../../lib/site-lib'); -class SiteLinkAdminController extends SiteController { +class AdminSiteLinkController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -103,8 +103,8 @@ class SiteLinkAdminController extends SiteController { } module.exports = { - name: 'adminSiteLink', - slug: 'admin-site-link', - className: 'SiteLinkAdminController', - create: async (dtp) => { return new SiteLinkAdminController(dtp); }, + logId: 'ctl:admin:site-link', + index: 'adminSiteLink', + className: 'AdminSiteLinkController', + create: async (dtp) => { return new AdminSiteLinkController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/user.js b/app/controllers/admin/user.js index b851b7e..3545f43 100644 --- a/app/controllers/admin/user.js +++ b/app/controllers/admin/user.js @@ -8,13 +8,21 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class UserAdminController extends SiteController { +class AdminUserController extends SiteController { constructor (dtp) { super(dtp, module.exports); } async start ( ) { + const { jobQueue: jobQueueService } = this.dtp.services; + + this.jobQueues = { }; + this.jobQueues.reeeper = await jobQueueService.getJobQueue( + 'reeeper', + this.dtp.config.jobQueues.reeeper, + ); + const router = express.Router(); router.use(async (req, res, next) => { res.locals.currentView = 'admin'; @@ -23,11 +31,26 @@ class UserAdminController extends SiteController { }); router.param('localUserId', this.populateLocalUserId.bind(this)); + router.param('archiveJobId', this.populateArchiveJobId.bind(this)); + router.param('archiveId', this.populateArchiveId.bind(this)); + + router.post('/local/:localUserId/archive', this.postArchiveLocalUser.bind(this)); router.post('/local/:localUserId', this.postUpdateLocalUser.bind(this)); + + router.get('/local/:localUserId/archive/confirm', this.getArchiveLocalUserConfirm.bind(this)); router.get('/local/:localUserId', this.getLocalUserView.bind(this)); + router.get('/archive/job/:archiveJobId', this.getUserArchiveJobView.bind(this)); + + router.post('/archive/:archiveId/action', this.postArchiveAction.bind(this)); + router.get('/archive/:archiveId/file', this.getUserArchiveFile.bind(this)); + router.get('/archive/:archiveId', this.getUserArchiveView.bind(this)); + + router.get('/archive', this.getUserArchiveIndex.bind(this)); + router.get('/', this.getHomeView.bind(this)); + return router; } @@ -44,6 +67,72 @@ class UserAdminController extends SiteController { } } + async populateArchiveJobId (req, res, next, archiveJobId) { + try { + res.locals.job = await this.jobQueues.reeeper.getJob(archiveJobId); + if (!res.locals.job) { + throw new SiteError(404, 'Job not found'); + } + return next(); + } catch (error) { + this.log.error('failed to populate Bull queue job', { archiveJobId, error }); + return next(error); + } + } + + async populateArchiveId (req, res, next, archiveId) { + const { user: userService } = this.dtp.services; + try { + res.locals.archive = await userService.getArchiveById(archiveId); + if (!res.locals.archive) { + throw new SiteError(404, 'Archive not found'); + } + return next(); + } catch (error) { + this.log.error('failed to populate UserArchive', { archiveId, error }); + return next(error); + } + } + + async postArchiveLocalUser (req, res, next) { + const { + logan: loganService, + user: userService, + } = this.dtp.services; + try { + + const user = await userService.getLocalUserAccount(req.body.userId); + if (!user) { + throw new SiteError(404, 'User not found'); + } + if (req.user && req.user._id.equals(user._id)) { + throw new SiteError(400, "You can't archive yourself"); + } + res.locals.job = await userService.archiveLocalUser(user); + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postArchiveUser', + data: { + job: res.locals.job.id, + user: user, + }, + }); + res.redirect(`/admin/user/archive/job/${res.locals.job.id}`); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postArchiveUser', + data: { + offender: { + _id: req.body.userId, + }, + error, + }, + }); + return next(error); + } + } + async postUpdateLocalUser (req, res, next) { const { logan: loganService, @@ -53,6 +142,11 @@ class UserAdminController extends SiteController { this.log.debug('local user update', { action: req.body.action }); switch (req.body.action) { case 'update': + if (req.user._id.equals(res.locals.userAccount._id)) { + if (req.user.flags.isAdmin && (req.body.isAdmin !== 'on')) { + throw new SiteError(400, "You can't remove your own admin privileges"); + } + } await userService.updateLocalForAdmin(res.locals.userAccount, req.body); loganService.sendRequestEvent(module.exports, req, { level: 'info', @@ -68,6 +162,9 @@ class UserAdminController extends SiteController { break; case 'ban': + if (req.user._id.equals(res.locals.userAccount._id)) { + throw new SiteError(400, "You can't ban yourself"); + } await userService.ban(res.locals.userAccount); loganService.sendRequestEvent(module.exports, req, { level: 'info', @@ -100,6 +197,133 @@ class UserAdminController extends SiteController { } } + async getUserArchiveJobView (req, res) { + res.locals.adminView = 'user-archive'; + res.render('admin/user/archive/job'); + } + + async getArchiveLocalUserConfirm (req, res) { + res.locals.adminView = 'user-archive'; + res.render('admin/user/archive/confirm'); + } + + async postArchiveAction (req, res, next) { + const { + logan: loganService, + user: userService, + } = this.dtp.services; + try { + switch (req.body.action) { + case 'update': + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postArchiveAction', + message: 'updating user archive record', + data: { + archive: { + _id: res.locals.archive._id, + user: { + _id: res.locals.archive.user._id, + username: res.locals.archive.user.username, + }, + }, + }, + }); + await userService.updateArchive(res.locals.archive, req.body); + return res.redirect(`/admin/user/archive/${res.locals.archive._id}`); + + case 'delete-file': + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postArchiveAction', + message: 'removing user archive file', + data: { + archive: { + _id: res.locals.archive._id, + user: { + _id: res.locals.archive.user._id, + username: res.locals.archive.user.username, + }, + }, + }, + }); + await userService.deleteArchiveFile(res.locals.archive); + return res.redirect(`/admin/user/archive/${res.locals.archive._id}`); + + case 'delete': + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postArchiveAction', + message: 'removing user archive', + data: { + archive: { + _id: res.locals.archive._id, + user: { + _id: res.locals.archive.user._id, + username: res.locals.archive.user.username, + }, + }, + }, + }); + await userService.deleteArchive(res.locals.archive); + return res.redirect(`/admin/user/archive`); + + default: + // unknown/invalid action + break; + } + + throw new SiteError(400, `Invalid user archive action: ${req.body.action}`); + + } catch (error) { + this.log.error('failed to delete archive file', { error }); + return next(error); + } + } + + async getUserArchiveFile (req, res, next) { + const { minio: minioService } = this.dtp.services; + try { + res.locals.adminView = 'user-archive'; + + this.log.debug('archive', { archive: res.locals.archive }); + const stream = await minioService.openDownloadStream({ + bucket: res.locals.archive.archive.bucket, + key: res.locals.archive.archive.key, + }); + + res.status(200); + res.set('Content-Type', 'application/zip'); + res.set('Content-Size', res.locals.archive.archive.size); + res.set('Content-Disposition', `attachment; filename="user-${res.locals.archive.user._id}.zip"`); + + stream.pipe(res); + } catch (error) { + this.log.error('failed to stream user archive file', { error }); + return next(error); + } + } + + async getUserArchiveView (req, res) { + res.locals.adminView = 'user-archive'; + res.render('admin/user/archive/view'); + } + + async getUserArchiveIndex (req, res, next) { + const { user: userService } = this.dtp.services; + try { + res.locals.adminView = 'user-archive'; + + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.archive = await userService.getArchives(res.locals.pagination); + + res.render('admin/user/archive/index'); + } catch (error) { + this.log.error('failed to render the User archives index', { error }); + return next(error); + } + } + async getHomeView (req, res, next) { const { user: userService } = this.dtp.services; try { @@ -114,8 +338,8 @@ class UserAdminController extends SiteController { } module.exports = { - name: 'adminUser', - slug: 'admin-user', - className: 'UserAdminController', - create: async (dtp) => { return new UserAdminController(dtp); }, + logId: 'ctl:admin:user', + index: 'adminUser', + className: 'AdminUserController', + create: async (dtp) => { return new AdminUserController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/admin/venue.js b/app/controllers/admin/venue.js index c76f737..f46e743 100644 --- a/app/controllers/admin/venue.js +++ b/app/controllers/admin/venue.js @@ -8,7 +8,7 @@ const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); -class VenueAdminController extends SiteController { +class AdminVenueController extends SiteController { constructor (dtp) { super(dtp, module.exports); @@ -136,8 +136,8 @@ class VenueAdminController extends SiteController { } module.exports = { - name: 'adminVenue', - slug: 'admin-venue', - className: 'VenueAdminController', - create: async (dtp) => { return new VenueAdminController(dtp); }, + logId: 'ctl:admin:venue', + index: 'adminVenue', + className: 'AdminVenueController', + create: async (dtp) => { return new AdminVenueController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/announcement.js b/app/controllers/announcement.js index c07ebbc..fcc0869 100644 --- a/app/controllers/announcement.js +++ b/app/controllers/announcement.js @@ -76,8 +76,8 @@ class AnnouncementController extends SiteController { } module.exports = { - slug: 'announcement', - name: 'announcement', + logId: 'ctl:announcement', + index: 'announcement', className: 'AnnouncementController', create: async (dtp) => { return new AnnouncementController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/auth.js b/app/controllers/auth.js index 57e80d0..9722902 100644 --- a/app/controllers/auth.js +++ b/app/controllers/auth.js @@ -339,8 +339,8 @@ class AuthController extends SiteController { } module.exports = { - slug: 'auth', - name: 'auth', + logId: 'ctl:auth', + index: 'auth', className: 'AuthController', create: async (dtp) => { return new AuthController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/author.js b/app/controllers/author.js index 4c7b732..afa0ce4 100644 --- a/app/controllers/author.js +++ b/app/controllers/author.js @@ -188,10 +188,8 @@ class AuthorController extends SiteController { } module.exports = { - slug: 'author', - name: 'author', - create: async (dtp) => { - let controller = new AuthorController(dtp); - return controller; - }, + logId: 'ctl:author', + index: 'author', + className: 'AuthorController', + create: async (dtp) => { return new AuthorController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/chat.js b/app/controllers/chat.js index 82cf2a8..11c3943 100644 --- a/app/controllers/chat.js +++ b/app/controllers/chat.js @@ -366,16 +366,21 @@ class ChatController extends SiteController { async getRoomEditor (req, res) { const { logan: loganService } = this.dtp.services; + + const logData = { }; + if (res.locals.room) { + logData.room = { + _id: res.locals.room._id, + name: res.locals.room.name, + }; + } + loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'getRoomEditor', - data: { - room: { - _id: res.locals.room._id, - name: res.locals.room.name, - }, - }, + data: logData, }); + res.render('chat/room/editor'); } @@ -643,8 +648,8 @@ class ChatController extends SiteController { } module.exports = { - slug: 'chat', - name: 'chat', + logId: 'ctl:chat', + index: 'chat', className: 'ChatController', create: async (dtp) => { return new ChatController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/comment.js b/app/controllers/comment.js index 17d8005..3b5f18a 100644 --- a/app/controllers/comment.js +++ b/app/controllers/comment.js @@ -25,7 +25,7 @@ class CommentController extends SiteController { dtp.app.use('/comment', router); router.use(async (req, res, next) => { - res.locals.currentView = module.exports.slug; + res.locals.currentView = module.exports.logId; return next(); }); @@ -151,8 +151,8 @@ class CommentController extends SiteController { } module.exports = { - slug: 'comment', - name: 'comment', + logId: 'ctl:comment', + index: 'comment', className: 'CommentController', create: async (dtp) => { return new CommentController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/content-report.js b/app/controllers/content-report.js index c9dda1d..7f64d5e 100644 --- a/app/controllers/content-report.js +++ b/app/controllers/content-report.js @@ -100,10 +100,8 @@ class ContentReportController extends SiteController { } module.exports = { - slug: 'content-report', - name: 'contentReport', - create: async (dtp) => { - let controller = new ContentReportController(dtp); - return controller; - }, + logId: 'svc:content-report', + index: 'contentReport', + className: 'ContentReportController', + create: async (dtp) => { return new ContentReportController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/email.js b/app/controllers/email.js index b70d6b1..27b0fd4 100644 --- a/app/controllers/email.js +++ b/app/controllers/email.js @@ -81,8 +81,8 @@ class EmailController extends SiteController { } module.exports = { - slug: 'email', - name: 'email', + logId: 'ctl:email', + index: 'email', className: 'EmailController', create: async (dtp) => { return new EmailController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/feed.js b/app/controllers/feed.js index cd91977..b269865 100644 --- a/app/controllers/feed.js +++ b/app/controllers/feed.js @@ -86,7 +86,8 @@ class SiteFeedController extends SiteController { } module.exports = { - slug: 'feed', - name: 'feed', + logId: 'ctl:feed', + index: 'feed', + className: 'SiteFeedController', create: async (dtp) => { return new SiteFeedController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/form.js b/app/controllers/form.js index f4b8c31..7b5736c 100644 --- a/app/controllers/form.js +++ b/app/controllers/form.js @@ -41,7 +41,7 @@ class FormController extends SiteController { sessionService.authCheckMiddleware({ requireLogin: true }), chatService.middleware({ maxOwnedRooms: 25, maxJoinedRooms: 50 }), async (req, res, next) => { - res.locals.currentView = module.exports.slug; + res.locals.currentView = module.exports.logId; return next(); }, ); @@ -64,8 +64,8 @@ class FormController extends SiteController { } module.exports = { - slug: 'form', - name: 'form', + logId: 'ctl:form', + index: 'form', className: 'FormController', create: async (dtp) => { return new FormController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/hive.js b/app/controllers/hive.js index b02891e..fb9be44 100644 --- a/app/controllers/hive.js +++ b/app/controllers/hive.js @@ -47,7 +47,7 @@ class HiveController extends SiteController { res.locals.hiveView = 'home'; res.status(200).json({ pkg: { name: this.dtp.pkg.name, version: this.dtp.pkg.version }, - component: { name: this.component.name, slug: this.component.slug }, + component: this.component, host: this.dtp.pkg.name, description: this.dtp.pkg.description, version: this.dtp.pkg.version, @@ -57,8 +57,8 @@ class HiveController extends SiteController { } module.exports = { - slug: 'hive', - name: 'hive', + logId: 'ctl:hive', + index: 'hive', className: 'HiveController', create: async (dtp) => { return new HiveController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/hive/kaleidoscope.js b/app/controllers/hive/kaleidoscope.js index 3dcd8dd..0e2eb21 100644 --- a/app/controllers/hive/kaleidoscope.js +++ b/app/controllers/hive/kaleidoscope.js @@ -93,8 +93,8 @@ class HiveKaleidoscopeController extends SiteController { } module.exports = { - name: 'hiveKaleidoscope', - slug: 'hive-kaleidoscope', + logId: 'ctl:hive:kaleidoscope', + index: 'hiveKaleidoscope', className: 'HiveKaleidoscopeController', create: async (dtp) => { return new HiveKaleidoscopeController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/hive/user.js b/app/controllers/hive/user.js index 4b7ab59..940a498 100644 --- a/app/controllers/hive/user.js +++ b/app/controllers/hive/user.js @@ -55,7 +55,7 @@ class HiveUserController extends SiteController { const { user: userService } = this.dtp.services; try { userId = mongoose.Types.ObjectId(userId); - res.locals.userProfile = await userService.getUserProfile(userId); + res.locals.userProfile = await userService.getLocalUserProfile(userId); if (!res.locals.userProfile) { throw new SiteError(404, 'User profile not found'); } @@ -80,7 +80,7 @@ class HiveUserController extends SiteController { res.locals.q = userService.filterUsername(req.query.q); res.locals.pagination = this.getPaginationParameters(req, 20); - res.locals.userProfiles = await userService.getUserAccounts(res.locals.pagination, res.locals.q); + res.locals.userProfiles = await userService.searchLocalUserAccounts(res.locals.pagination, res.locals.q); res.locals.userProfiles = res.locals.userProfiles.map((user) => { const apiUser = userService.filterUserObject(user); apiUser.picture.large = `/image/${user.picture.large}`; @@ -149,8 +149,8 @@ class HiveUserController extends SiteController { } module.exports = { - name: 'hiveUser', - slug: 'hive-user', + logId: 'ctl:hive:user', + index: 'hiveUser', className: 'HiveUserController', create: async (dtp) => { return new HiveUserController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/home.js b/app/controllers/home.js index e2dc895..56dd806 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -96,9 +96,10 @@ class HomeController extends SiteController { } module.exports = { - isHome: true, - slug: 'home', - name: 'home', + logId: 'ctl:home', + index: 'home', className: 'HomeController', create: async (dtp) => { return new HomeController(dtp); }, + + isHome: true, }; \ No newline at end of file diff --git a/app/controllers/image.js b/app/controllers/image.js index 83a38f9..543f33f 100644 --- a/app/controllers/image.js +++ b/app/controllers/image.js @@ -9,7 +9,7 @@ const fs = require('fs'); const express = require('express'); const mongoose = require('mongoose'); -const { SiteController/*, SiteError*/ } = require('../../lib/site-lib'); +const { SiteController, SiteError } = require('../../lib/site-lib'); class ImageController extends SiteController { @@ -60,6 +60,9 @@ class ImageController extends SiteController { try { res.locals.imageId = mongoose.Types.ObjectId(imageId); res.locals.image = await this.dtp.services.image.getImageById(res.locals.imageId); + if (!res.locals.image) { + throw new SiteError(404, 'Image not found'); + } return next(); } catch (error) { this.log.error('failed to populate image', { error }); @@ -132,8 +135,8 @@ class ImageController extends SiteController { } module.exports = { - slug: 'image', - name: 'image', + logId: 'ctl:image', + index: 'image', className: 'ImageController', create: async (dtp) => { return new ImageController(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/controllers/manifest.js b/app/controllers/manifest.js index d64b28d..51c2cad 100644 --- a/app/controllers/manifest.js +++ b/app/controllers/manifest.js @@ -22,7 +22,7 @@ class ManifestController extends SiteController { dtp.app.use('/manifest.json', router); router.use(async (req, res, next) => { - res.locals.currentView = this.component.slug; + res.locals.currentView = this.component.logId; return next(); }); @@ -64,8 +64,8 @@ class ManifestController extends SiteController { } module.exports = { - slug: 'manifest', - name: 'manifest', + logId: 'ctl:manifest', + index: 'manifest', className: 'ManifestController', create: async (dtp) => { return new ManifestController(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/controllers/newsletter.js b/app/controllers/newsletter.js index 112d5b9..419eff6 100644 --- a/app/controllers/newsletter.js +++ b/app/controllers/newsletter.js @@ -24,7 +24,7 @@ class NewsletterController extends SiteController { dtp.app.use('/newsletter', router); router.use(async (req, res, next) => { - res.locals.currentView = module.exports.slug; + res.locals.currentView = module.exports.logId; return next(); }); @@ -92,8 +92,8 @@ class NewsletterController extends SiteController { } module.exports = { - slug: 'newsletter', - name: 'newsletter', + logId: 'ctl:newsletter', + index: 'newsletter', className: 'NewsletterController', create: async (dtp) => { return new NewsletterController(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/controllers/newsroom.js b/app/controllers/newsroom.js index 3f83207..3fce1ed 100644 --- a/app/controllers/newsroom.js +++ b/app/controllers/newsroom.js @@ -17,30 +17,39 @@ class NewsroomController extends SiteController { async start ( ) { const { dtp } = this; const { limiter: limiterService } = dtp.services; + const limiterConfig = limiterService.config.newsroom; const router = express.Router(); dtp.app.use('/newsroom', router); router.use(async (req, res, next) => { - res.locals.currentView = module.exports.slug; + res.locals.currentView = module.exports.logId; return next(); }); router.param('feedId', this.populateFeedId.bind(this)); + router.get('/feed', + limiterService.createMiddleware(limiterConfig.getUnifiedFeed), + this.getUnifiedFeed.bind(this), + ); + router.get('/:feedId', - limiterService.createMiddleware(limiterService.config.newsroom.getFeedView), + limiterService.createMiddleware(limiterConfig.getFeedView), this.getFeedView.bind(this), ); router.get('/', - limiterService.createMiddleware(limiterService.config.newsletter.getIndex), + limiterService.createMiddleware(limiterConfig.getIndex), this.getHome.bind(this), ); } async populateFeedId (req, res, next, feedId) { - const { feed: feedService } = this.dtp.services; + const { + feed: feedService, + logan: loganService, + } = this.dtp.services; try { res.locals.feed = await feedService.getById(feedId); if (!res.locals.feed) { @@ -48,39 +57,116 @@ class NewsroomController extends SiteController { } return next(); } catch (error) { - this.log.error('failed to populate feedId', { feedId, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populateFeedId', + message: error.message, + data: { feedId, error }, + }); return next(error); } } + async getUnifiedFeed (req, res) { + const { + feed: feedService, + logan: loganService, + } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.newsroom = await feedService.getNewsfeed(res.locals.pagination); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getUnifiedFeed', + data: { fmt: req.query.fmt || 'html' }, + }); + + switch (req.query.fmt) { + case 'json': + res.status(200).json(res.locals.newsroom); + break; + + default: + res.render('newsroom/unified-feed'); + break; + } + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getUnifiedFeed', + message: error.message, + data: { error }, + }); + res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } + async getFeedView (req, res, next) { - const { feed: feedService } = this.dtp.services; + const { + feed: feedService, + logan: loganService, + } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 10); res.locals.newsroom = await feedService.getFeedEntries(res.locals.feed, res.locals.pagination); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getFeedView', + data: { + feed: { + _id: res.locals.feed._id, + title: res.locals.feed.title, + }, + }, + }); + res.render('newsroom/feed-view'); } catch (error) { - this.log.error('failed to present newsroom home', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getFeedView', + message: error.message, + data: { error }, + }); return next(error); } } async getHome (req, res, next) { - const { feed: feedService } = this.dtp.services; + const { + feed: feedService, + logan: loganService, + } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 12); res.locals.newsroom = await feedService.getFeeds(res.locals.pagination, { withEntries: true }); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getHome', + }); + res.render('newsroom/index'); } catch (error) { - this.log.error('failed to present newsroom home', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getHome', + message: error.message, + data: { error }, + }); return next(error); } } } module.exports = { - slug: 'newsroom', - name: 'newsroom', + logId: 'ctl:newsroom', + index: 'newsroom', className: 'NewsroomController', create: (dtp) => { return new NewsroomController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/notification.js b/app/controllers/notification.js index ae89916..fbca3d0 100644 --- a/app/controllers/notification.js +++ b/app/controllers/notification.js @@ -72,8 +72,8 @@ class NotificationController extends SiteController { } module.exports = { - slug: 'notification', - name: 'notification', + logId: 'ctl:notification', + index: 'notification', className: 'NotificationController', create: async (dtp) => { return new NotificationController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/page.js b/app/controllers/page.js index 12703c2..9682e3e 100644 --- a/app/controllers/page.js +++ b/app/controllers/page.js @@ -35,7 +35,10 @@ class PageController extends SiteController { } async populatePageSlug (req, res, next, pageSlug) { - const { page: pageService } = this.dtp.services; + const { + logan: loganService, + page: pageService, + } = this.dtp.services; try { res.locals.page = await pageService.getBySlug(pageSlug); if (!res.locals.page) { @@ -43,28 +46,48 @@ class PageController extends SiteController { } return next(); } catch (error) { - this.log.error('failed to populate pageSlug', { pageSlug, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populatePageSlug', + message: error.message, + data: { error }, + }); return next(error); } } async getView (req, res, next) { - const { resource: resourceService } = this.dtp.services; + const { + logan: loganService, + resource: resourceService, + } = this.dtp.services; try { await resourceService.recordView(req, 'Page', res.locals.page._id); res.locals.pageSlug = res.locals.page.slug; res.locals.pageTitle = `${res.locals.page.title} on ${this.dtp.config.site.name}`; + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getView', + }); + res.render('page/view'); } catch (error) { this.log.error('failed to service page view', { pageId: res.locals.page._id, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getView', + message: error.message, + data: { error }, + }); return next(error); } } } module.exports = { - slug: 'page', - name: 'page', + logId: 'ctl:page', + index: 'page', className: 'PageController', create: async (dtp) => { return new PageController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/post.js b/app/controllers/post.js index f1c62b9..91bb082 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -93,7 +93,6 @@ class PostController extends SiteController { router.delete( '/:postId/profile-photo', - // limiterService.createMiddleware(limiterService.config.post.deletePostFeatureImage), requireAuthorPrivileges, this.deletePostFeatureImage.bind(this), ); @@ -103,14 +102,15 @@ class PostController extends SiteController { requireAuthorPrivileges, this.deletePost.bind(this), ); - - - + router.get('/tag/:tagSlug', this.getTagSearchView.bind(this)); } async populateUsername (req, res, next, username) { - const { user: userService } = this.dtp.services; + const { + logan: loganService, + user: userService, + } = this.dtp.services; try { res.locals.author = await userService.lookup(username); if (!res.locals.author) { @@ -118,13 +118,21 @@ class PostController extends SiteController { } return next(); } catch (error) { - this.log.error('failed to populate username', { username, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populateUsername', + message: error.message, + data: { error }, + }); return next(error); } } async populatePostSlug (req, res, next, postSlug) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { res.locals.post = await postService.getBySlug(postSlug); if (!res.locals.post) { @@ -132,13 +140,21 @@ class PostController extends SiteController { } return next(); } catch (error) { - this.log.error('failed to populate postSlug', { postSlug, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populatePostSlug', + message: error.message, + data: { error }, + }); return next(error); } } async populatePostId (req, res, next, postId) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { res.locals.post = await postService.getById(postId); @@ -147,13 +163,55 @@ class PostController extends SiteController { return next(); } catch (error) { - this.log.error('failed to populate postId', { postId, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populatePostId', + message: error.message, + data: { error }, + }); + return next(error); + } + } + + async populateTagSlug (req, res, next, tagSlug) { + const { + logan: loganService, + post: postService, + } = this.dtp.services; + try { + var allPosts = false; + var statusArray = ['published']; + if (req.user) { + if (req.user.flags.isAdmin) { + statusArray.push('draft'); + allPosts = true; + } + } + res.locals.allPosts = allPosts; + res.locals.tagSlug = tagSlug; + res.locals.tag = tagSlug.replace("_", " "); + res.locals.pagination = this.getPaginationParameters(req, 12); + const {posts, totalPosts} = await postService.getByTags(res.locals.tag, res.locals.pagination, statusArray); + res.locals.posts = posts; + res.locals.totalPosts = totalPosts; + return next(); + + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populateTagSlug', + message: error.message, + data: { error }, + }); return next(error); } } async postBlockCommentAuthor (req, res) { - const { user: userService } = this.dtp.services; + const { + logan: loganService, + user: userService, + } = this.dtp.services; try { const displayList = this.createDisplayList('add-recipient'); await userService.blockUser(req.user._id, req.body.userId); @@ -163,9 +221,27 @@ class PostController extends SiteController { 'bottom-center', 4000, ); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postBlockCommentAuthor', + data: { + post: { + _id: res.locals.post._id, + title: res.locals.post.title, + }, + blockedUserId: req.body.userId, + }, + }); + res.status(200).json({ success: true, displayList }); } catch (error) { - this.log.error('failed to report comment', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postBlockCommentAuthor', + message: error.message, + data: { error }, + }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, @@ -174,7 +250,10 @@ class PostController extends SiteController { } async postComment (req, res) { - const { comment: commentService } = this.dtp.services; + const { + comment: commentService, + logan: loganService, + } = this.dtp.services; try { const displayList = this.createDisplayList('add-recipient'); @@ -202,14 +281,38 @@ class PostController extends SiteController { 'bottom-center', 4000, ); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postComment', + data: { + post: { + _id: res.locals.post._id, + title: res.locals.post.title, + }, + comment: { + _id: res.locals.comment._id, + }, + }, + }); + res.status(200).json({ success: true, displayList }); } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postComment', + message: error.message, + data: { error }, + }); res.status(error.statusCode || 500).json({ success: false, message: error.message }); } } async postUpdateImage (req, res) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { const displayList = this.createDisplayList('post-image'); @@ -221,9 +324,26 @@ class PostController extends SiteController { 'bottom-center', 2000, ); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postUpdateImage', + data: { + post: { + _id: res.locals.post._id, + title: res.locals.post.title, + }, + }, + }); + res.status(200).json({ success: true, displayList }); } catch (error) { - this.log.error('failed to update feature image', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postUpdateImage', + message: error.message, + data: { error }, + }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, @@ -232,11 +352,22 @@ class PostController extends SiteController { } async deletePostFeatureImage (req, res) { + const { logan: loganService } = this.dtp.services; + + loganService.sendRequestEvent(module.exports, req, { + level: 'alert', + message: 'Deleting a post feature image is not yet implemented', + event: 'deletePostFeatureImage', + }); + res.status(500).json({ success: false, message: 'Removing the featured image is not yet implemented'}); } async postUpdatePost (req, res, next) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { if(!req.user.flags.isAdmin){ if (!req.user._id.equals(res.locals.post.author._id) || @@ -245,15 +376,35 @@ class PostController extends SiteController { } } await postService.update(req.user, res.locals.post, req.body); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postUpdatePost', + data: { + post: { + _id: res.locals.post._id, + title: res.locals.post.title, + }, + }, + }); + res.redirect(`/post/${res.locals.post.slug}`); } catch (error) { - this.log.error('failed to update post', { postId: res.locals.post._id, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postUpdatePost', + message: error.message, + data: { error }, + }); return next(error); } } async postUpdatePostTags (req, res) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { if(!req.user.flags.isAdmin) { @@ -269,9 +420,26 @@ class PostController extends SiteController { 'bottom-center', 2000, ); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postUpdatePostTags', + data: { + post: { + _id: res.locals.post._id, + title: res.locals.post.title, + }, + }, + }); + res.status(200).json({ success: true, displayList }); } catch (error) { - this.log.error('failed to update post tags', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postUpdatePostTags', + message: error.message, + data: { error }, + }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, @@ -280,18 +448,41 @@ class PostController extends SiteController { } async postCreatePost (req, res, next) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { res.locals.post = await postService.create(req.user, req.body); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'postCreatePost', + data: { + post: { + _id: res.locals.post._id, + title: res.locals.post.title, + }, + }, + }); + res.redirect(`/post/${res.locals.post.slug}`); } catch (error) { - this.log.error('failed to create post', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postCreatePost', + message: error.message, + data: { error }, + }); return next(error); } } async getComments (req, res) { - const { comment: commentService } = this.dtp.services; + const { + comment: commentService, + logan: loganService, + } = this.dtp.services; try { const displayList = this.createDisplayList('add-recipient'); @@ -322,9 +513,19 @@ class PostController extends SiteController { const replyList = `ul#post-comment-list`; displayList.addElement(replyList, 'beforeEnd', html); + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getComments', + }); + res.status(200).json({ success: true, displayList }); } catch (error) { - this.log.error('failed to fetch more comments', { postId: res.locals.post._id, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getComments', + message: error.message, + data: { error }, + }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, @@ -333,7 +534,11 @@ class PostController extends SiteController { } async getView (req, res, next) { - const { comment: commentService, resource: resourceService } = this.dtp.services; + const { + comment: commentService, + logan: loganService, + resource: resourceService, + } = this.dtp.services; try { if (res.locals.post.status !== 'published') { if (!req.user) { @@ -363,68 +568,156 @@ class PostController extends SiteController { if (res.locals.post.image) { res.locals.shareImage = `https://${this.dtp.config.site.domain}/image/${res.locals.post.image._id}`; } + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getView', + }); + res.render('post/view'); } catch (error) { - this.log.error('failed to service post view', { postId: res.locals.post._id, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getView', + message: error.message, + data: { error }, + }); return next(error); } } - async getEditor (req, res) { - res.render('post/editor'); + async getEditor (req, res, next) { + const { logan: loganService } = this.dtp.services; + try { + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getEditor', + }); + res.render('post/editor'); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getEditor', + message: error.message, + data: { error }, + }); + return next(error); + } } async getComposer (req, res, next) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { res.locals.post = await postService.createPlaceholder(req.user); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getComposer', + }); + res.redirect(`/post/${res.locals.post._id}/edit`); } catch (error) { - this.log.error('failed to render post composer', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getComposer', + message: error.message, + data: { error }, + }); return next(error); } } async getIndex (req, res, next) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.posts = await postService.getPosts(res.locals.pagination); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getIndex', + }); + res.render('post/index'); } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getIndex', + message: error.message, + data: { error }, + }); return next(error); } } async getAuthorView (req, res, next) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 20); const {posts, totalPostCount} = await postService.getForAuthor(res.locals.author, ['published'], res.locals.pagination); res.locals.posts = posts; res.locals.totalPostCount = totalPostCount; + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getAuthorView', + }); + res.render('post/author/view'); } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getAuthorView', + message: error.message, + data: { error }, + }); return next(error); } } async getAllAuthorsView (req, res, next) { - const { user: userService } = this.dtp.services; + const { + logan: loganService, + user: userService, + } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 20); const {authors , totalAuthorCount } = await userService.getAuthors(res.locals.pagination); res.locals.authors = authors; res.locals.totalAuthorCount = totalAuthorCount; + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getAllAuthorsView', + }); + res.render('post/author/all'); } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getAllAuthorsView', + message: error.message, + data: { error }, + }); return next(error); } } async deletePost (req, res) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { // only give admins and the author permission to delete if (!req.user.flags.isAdmin) { @@ -438,9 +731,29 @@ class PostController extends SiteController { const displayList = this.createDisplayList('add-recipient'); displayList.navigateTo('/'); + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'deletePost', + data: { + post: { + _id: res.locals.post._id, + title: res.locals.post.title, + author: { + _id: res.locals.post.author._id, + username: res.locals.post.author.username, + }, + }, + }, + }); + res.status(200).json({ success: true, displayList }); } catch (error) { - this.log.error('failed to remove post', { newletterId: res.locals.post._id, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'deletePost', + message: error.message, + data: { error }, + }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, @@ -448,36 +761,16 @@ class PostController extends SiteController { } } - async populateTagSlug (req, res, next, tagSlug) { - const { post: postService } = this.dtp.services; - try { - var allPosts = false; - var statusArray = ['published']; - if (req.user) { - if (req.user.flags.isAdmin) { - statusArray.push('draft'); - allPosts = true; - } - } - res.locals.allPosts = allPosts; - res.locals.tagSlug = tagSlug; - res.locals.tag = tagSlug.replace("_", " "); - res.locals.pagination = this.getPaginationParameters(req, 12); - const {posts, totalPosts} = await postService.getByTags(res.locals.tag, res.locals.pagination, statusArray); - res.locals.posts = posts; - res.locals.totalPosts = totalPosts; - return next(); - - } catch (error) { - this.log.error('failed to populate tagSlug', { tagSlug, error }); - return next(error); - } - } - async getTagSearchView (req, res) { + const { logan: loganService } = this.dtp.services; try { res.locals.pageTitle = `Tag ${res.locals.tag} on ${this.dtp.config.site.name}`; + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getTagSearchView', + }); + res.render('post/tag/view'); } catch (error) { this.log.error('failed to service post view', { postId: res.locals.post._id, error }); @@ -487,21 +780,29 @@ class PostController extends SiteController { async getTagIndex (req, res, next) { - const { post: postService } = this.dtp.services; + const { + logan: loganService, + post: postService, + } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.posts = await postService.getPosts(res.locals.pagination); + + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getTagIndex', + }); + res.render('post/tag/index'); } catch (error) { return next(error); } } - } module.exports = { - slug: 'post', - name: 'post', + logId: 'ctl:post', + index: 'post', className: 'PostController', create: async (dtp) => { return new PostController(dtp); }, }; \ No newline at end of file diff --git a/app/controllers/user.js b/app/controllers/user.js index ebf8793..205b5b4 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -119,13 +119,13 @@ class UserController extends SiteController { ); router.get( - '/:userId/otp-setup', + '/:localUserId/otp-setup', limiterService.createMiddleware(limiterService.config.user.getOtpSetup), otpSetup, this.getOtpSetup.bind(this), ); router.get( - '/:userId/otp-disable', + '/:localUserId/otp-disable', limiterService.createMiddleware(limiterService.config.user.getOtpDisable), authRequired, this.getOtpDisable.bind(this), @@ -148,7 +148,7 @@ class UserController extends SiteController { ); router.delete( - '/:userId/profile-photo', + '/:localUserId/profile-photo', limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto), authRequired, checkProfileOwner, @@ -334,6 +334,12 @@ class UserController extends SiteController { }); } catch (error) { this.log.error('failed to create new user', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'postCreateUser', + message: `failed to create user account: ${error.message}`, + data: { definition: req.body, error }, + }); return next(error); } } @@ -639,8 +645,8 @@ class UserController extends SiteController { } module.exports = { - slug: 'user', - name: 'user', + logId: 'ctl:user', + index: 'user', className: 'UserController', create: async (dtp) => { return new UserController(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/controllers/venue.js b/app/controllers/venue.js index a414431..289d5b3 100644 --- a/app/controllers/venue.js +++ b/app/controllers/venue.js @@ -40,7 +40,10 @@ class VenueController extends SiteController { } async populateChannelSlug (req, res, next, channelSlug) { - const { venue: venueService } = this.dtp.services; + const { + logan: loganService, + venue: venueService, + } = this.dtp.services; try { res.locals.channel = await venueService.getChannelBySlug(channelSlug, { withCredentials: true }); if (!res.locals.channel) { @@ -53,30 +56,59 @@ class VenueController extends SiteController { return next(); } catch (error) { this.log.error('failed to populate Venue channel by slug', { channelSlug, error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'populateChannelSlug', + message: error.message, + data: { channelSlug, error }, + }); return next(error); } } - async getVenueEmbed (req, res) { - res.render('venue/embed'); + async getVenueEmbed (req, res, next) { + const { logan: loganService } = this.dtp.services; + try { + loganService.sendRequestEvent(module.exports, req, { + level: 'info', + event: 'getVenueEmbed', + }); + res.render('venue/embed'); + } catch (error) { + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getView', + message: error.message, + data: { error }, + }); + return next(error); + } } async getHome (req, res, next) { - const { venue: venueService} = this.dtp.services; + const { + logan: loganService, + venue: venueService, + } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 10); res.locals.channels = await venueService.getChannels(res.locals.pagination); res.render('venue/index'); } catch (error) { - this.log.error('failed to present the Venue home', { error }); + loganService.sendRequestEvent(module.exports, req, { + level: 'error', + event: 'getHome', + message: error.message, + data: { error }, + }); return next(error); } } } module.exports = { - slug: 'venue', - name: 'venue', + logId: 'ctl:venue', + index: 'venue', className: 'VenueController', create: async (dtp) => { return new VenueController(dtp); }, }; diff --git a/app/controllers/welcome.js b/app/controllers/welcome.js index 97e2f42..3b0b401 100644 --- a/app/controllers/welcome.js +++ b/app/controllers/welcome.js @@ -9,7 +9,7 @@ const path = require('path'); const express = require('express'); const captcha = require('svg-captcha'); -const { SiteController/*, SiteError */ } = require('../../lib/site-lib'); +const { SiteController } = require('../../lib/site-lib'); class WelcomeController extends SiteController { @@ -120,8 +120,8 @@ class WelcomeController extends SiteController { } module.exports = { - slug: 'welcome', - name: 'welcome', + logId: 'ctl:welcome', + index: 'welcome', className: 'WelcomeController', create: async (dtp) => { return new WelcomeController(dtp); }, }; \ No newline at end of file diff --git a/app/models/log.js b/app/models/log.js index b74d682..e7a77f2 100644 --- a/app/models/log.js +++ b/app/models/log.js @@ -21,8 +21,9 @@ const LOG_LEVEL_LIST = [ const LogSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: -1, expires: '7d' }, component: { - name: { type: String, required: true }, - slug: { type: String, required: true, index: 1 }, + logId: { type: String, required: true, index: 1 }, + index: { type: String, required: true }, + className: { type: String, required: true, index: 1 }, }, level: { type: String, enum: LOG_LEVEL_LIST, required: true, index: true }, message: { type: String }, diff --git a/app/models/user-archive.js b/app/models/user-archive.js new file mode 100644 index 0000000..842fc67 --- /dev/null +++ b/app/models/user-archive.js @@ -0,0 +1,29 @@ +// user-archive.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const UserArchiveSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: -1 }, + user: { + _id: { type: Schema.ObjectId, required: true, index: 1 }, + email: { type: String }, + username: { type: String, required: true }, + }, + archive: { + bucket: { type: String, required: true }, + key: { type: String, required: true }, + etag: { type: String, required: true }, + size: { type: Number, required: true }, + }, + notes: { type: String }, +}); + +module.exports = (conn) => { + return conn.model('UserArchive', UserArchiveSchema); +}; \ No newline at end of file diff --git a/app/services/announcement.js b/app/services/announcement.js index 5e7969e..02e8277 100644 --- a/app/services/announcement.js +++ b/app/services/announcement.js @@ -116,8 +116,8 @@ class AnnouncementService extends SiteService { } module.exports = { - slug: 'announcement', - name: 'announcement', + logId: 'svc:announcement', + index: 'announcement', className: 'AnnouncementService', create: (dtp) => { return new AnnouncementService(dtp); }, }; \ No newline at end of file diff --git a/app/services/attachment.js b/app/services/attachment.js index 8c086f9..309e00f 100644 --- a/app/services/attachment.js +++ b/app/services/attachment.js @@ -126,6 +126,23 @@ class AttachmentService extends SiteService { return attachments; } + /** + * Access all attachments sorted by most recent with pagination. This is for + * use by Admin tools. + * @param {*} pagination required pagination parameters (skip and cpp) + * @returns Array of attachments + */ + async getRecent (pagination) { + const attachments = await Attachment + .find() + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .populate(this.populateAttachment) + .lean(); + return attachments; + } + /** * * @param {mongoose.Types.ObjectId} attachmentId The ID of the attachment @@ -166,6 +183,20 @@ class AttachmentService extends SiteService { return this.attachmentTemplate({ attachment, attachmentOptions }); } + /** + * Removes all attachments and everything on storage about them for a + * specified User. + * @param {User} owner the owner of the attachments to be removed + */ + async removeForOwner (owner) { + const handler = this.remove.bind(this); + await Attachment + .find({ owner: owner._id }) + .lean() + .cursor() + .eachAsync(handler); + } + /** * Creates a Bull Queue job to delete an Attachment including it's processed * and original media files. @@ -180,8 +211,8 @@ class AttachmentService extends SiteService { } module.exports = { - slug: 'attachment', - name: 'attachment', + logId: 'svc:attachment', + index: 'attachment', className: 'AttachmentService', create: (dtp) => { return new AttachmentService(dtp); }, }; \ No newline at end of file diff --git a/app/services/cache.js b/app/services/cache.js index 418752c..8da2605 100644 --- a/app/services/cache.js +++ b/app/services/cache.js @@ -57,8 +57,8 @@ class CacheService extends SiteService { } module.exports = { - slug: 'cache', - name: 'cache', + logId: 'svc:cache', + index: 'cache', className: 'CacheService', create: (dtp) => { return new CacheService(dtp); }, }; \ No newline at end of file diff --git a/app/services/chat.js b/app/services/chat.js index 64a24c3..1c4a744 100644 --- a/app/services/chat.js +++ b/app/services/chat.js @@ -785,6 +785,16 @@ class ChatService extends SiteService { return reaction.toObject(); } + async getRecent (limit = 10) { + const messages = await ChatMessage + .find({ }) + .sort({ created: -1 }) + .limit(limit) + .populate(this.populateChatMessage) + .lean(); + return messages; + } + async removeForUser (user) { const { logan: loganService } = this.dtp.services; @@ -852,8 +862,8 @@ class ChatService extends SiteService { } module.exports = { - slug: 'chat', - name: 'chat', + logId: 'svc:chat', + index: 'chat', className: 'ChatService', create: (dtp) => { return new ChatService(dtp); }, }; \ No newline at end of file diff --git a/app/services/comment.js b/app/services/comment.js index 8a3f115..0ea4158 100644 --- a/app/services/comment.js +++ b/app/services/comment.js @@ -251,6 +251,21 @@ class CommentService extends SiteService { return comments; } + /** + * Meant for use in admin tools. + */ + async getRecent (pagination) { + const comments = await Comment + .find() + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .populate(this.populateComment) + .lean(); + const totalCommentCount = await Comment.estimatedDocumentCount(); + return { comments, totalCommentCount }; + } + async getForAuthor (author, pagination) { const comments = await Comment .find({ // index: 'comment_author_by_type' @@ -335,8 +350,8 @@ class CommentService extends SiteService { } module.exports = { - slug: 'comment', - name: 'comment', + logId: 'svc:comment', + index: 'comment', className: 'CommentService', create: (dtp) => { return new CommentService(dtp); }, }; \ No newline at end of file diff --git a/app/services/content-report.js b/app/services/content-report.js index 3d2d8dc..59c2190 100644 --- a/app/services/content-report.js +++ b/app/services/content-report.js @@ -127,8 +127,8 @@ class ContentReportService extends SiteService { } module.exports = { - slug: 'content-report', - name: 'contentReport', + logId: 'svc:content-report', + index: 'contentReport', className: 'ContentReportService', create: (dtp) => { return new ContentReportService(dtp); }, }; \ No newline at end of file diff --git a/app/services/content-vote.js b/app/services/content-vote.js index c2cdc04..d5916eb 100644 --- a/app/services/content-vote.js +++ b/app/services/content-vote.js @@ -117,8 +117,8 @@ class ContentVoteService extends SiteService { } module.exports = { - slug: 'content-vote', - name: 'contentVote', + logId: 'svc:content-vote', + index: 'contentVote', className: 'ContentVoteService', create: (dtp) => { return new ContentVoteService(dtp); }, }; \ No newline at end of file diff --git a/app/services/core-node.js b/app/services/core-node.js index 5488630..3aa0148 100644 --- a/app/services/core-node.js +++ b/app/services/core-node.js @@ -716,8 +716,8 @@ class CoreNodeService extends SiteService { } module.exports = { - slug: 'core-node', - name: 'coreNode', + logId: 'svc:core-node', + index: 'coreNode', className: 'CoreNodeService', create: (dtp) => { return new CoreNodeService(dtp); }, }; \ No newline at end of file diff --git a/app/services/crypto.js b/app/services/crypto.js index d65f3bc..2325078 100644 --- a/app/services/crypto.js +++ b/app/services/crypto.js @@ -58,8 +58,8 @@ class CryptoService extends SiteService { } module.exports = { - slug: 'crypto', - name: 'crypto', + logId: 'svc:crypto', + index: 'crypto', className: 'CryptoService', create: (dtp) => { return new CryptoService(dtp); }, }; \ No newline at end of file diff --git a/app/services/csrf-token.js b/app/services/csrf-token.js index 764a50b..3594074 100644 --- a/app/services/csrf-token.js +++ b/app/services/csrf-token.js @@ -76,8 +76,8 @@ class CsrfTokenService extends SiteService { } module.exports = { - slug: 'csrf-token', - name: 'csrfToken', + logId: 'svc:csrf-token', + index: 'csrfToken', className: 'CsrfTokenService', create: (dtp) => { return new CsrfTokenService(dtp); }, }; \ No newline at end of file diff --git a/app/services/dashboard.js b/app/services/dashboard.js index 738df0a..7d7d6a7 100644 --- a/app/services/dashboard.js +++ b/app/services/dashboard.js @@ -270,8 +270,8 @@ class DashboardService extends SiteService { } module.exports = { - slug: 'dashboard', - name: 'dashboard', + logId: 'svc:dashboard', + index: 'dashboard', className: 'DashboardService', create: (dtp) => { return new DashboardService(dtp); }, }; \ No newline at end of file diff --git a/app/services/display-engine.js b/app/services/display-engine.js index 56296eb..bca2993 100644 --- a/app/services/display-engine.js +++ b/app/services/display-engine.js @@ -132,10 +132,6 @@ class DisplayEngineService extends SiteService { this.templates = { }; } - async start ( ) { } - - async stop ( ) { } - loadTemplate (name, pugScript) { const scriptFile = path.join(this.dtp.config.root, 'app', 'views', pugScript); this.templates[name] = pug.compileFile(scriptFile); @@ -154,8 +150,8 @@ class DisplayEngineService extends SiteService { } module.exports = { - slug: 'display-engine', - name: 'displayEngine', + logId: 'svc:display-engine', + index: 'displayEngine', className: 'DisplayEngineService', create: (dtp) => { return new DisplayEngineService(dtp); }, }; \ No newline at end of file diff --git a/app/services/email.js b/app/services/email.js index 19aef2f..76b6364 100644 --- a/app/services/email.js +++ b/app/services/email.js @@ -171,8 +171,8 @@ class EmailService extends SiteService { } module.exports = { - slug: 'email', - name: 'email', + logId: 'svc:email', + index: 'email', className: 'EmailService', create: (dtp) => { return new EmailService(dtp); }, }; \ No newline at end of file diff --git a/app/services/feed.js b/app/services/feed.js index 382b134..f2f2858 100644 --- a/app/services/feed.js +++ b/app/services/feed.js @@ -237,8 +237,8 @@ class FeedService extends SiteService { } module.exports = { - slug: 'feed', - name: 'feed', + logId: 'svc:feed', + index: 'feed', className: 'FeedService', create: (dtp) => { return new FeedService(dtp); }, }; \ No newline at end of file diff --git a/app/services/gab-tv.js b/app/services/gab-tv.js index 5a2a9a9..7d7824c 100644 --- a/app/services/gab-tv.js +++ b/app/services/gab-tv.js @@ -60,8 +60,8 @@ class GabTVService extends SiteService { } module.exports = { - slug: 'gab-tv', - name: 'gabTV', + logId: 'svc:gab-tv', + index: 'gabTV', className: 'GabTVService', create: (dtp) => { return new GabTVService(dtp); }, }; \ No newline at end of file diff --git a/app/services/hive.js b/app/services/hive.js index 67fd36e..ff67407 100644 --- a/app/services/hive.js +++ b/app/services/hive.js @@ -276,8 +276,8 @@ class HiveService extends SiteService { } module.exports = { - slug: 'hive', - name: 'hive', + logId: 'svc:hive', + index: 'hive', className: 'HiveService', create: (dtp) => { return new HiveService(dtp); }, }; \ No newline at end of file diff --git a/app/services/host-cache.js b/app/services/host-cache.js index 08339f5..d8d7d39 100644 --- a/app/services/host-cache.js +++ b/app/services/host-cache.js @@ -97,8 +97,8 @@ class HostCacheService extends SiteService { } module.exports = { - slug: 'host-cache', - name: 'hostCache', + logId: 'svc:host-cache', + index: 'hostCache', className: 'HostCacheService', create: (dtp) => { return new HostCacheService(dtp); }, }; \ No newline at end of file diff --git a/app/services/image.js b/app/services/image.js index eb3a4f2..c15b732 100644 --- a/app/services/image.js +++ b/app/services/image.js @@ -90,16 +90,37 @@ class ImageService extends SiteService { return image; } - async getRecentImagesForOwner(owner) { + async getRecentImagesForOwner(owner, limit = 10) { const images = await SiteImage .find({ owner: owner._id }) .sort({ created: -1 }) - .limit(10) + .limit(limit) .populate(this.populateImage) .lean(); return images; } + async getRecentImages (pagination) { + const images = await SiteImage + .find() + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .populate(this.populateImage) + .lean(); + const totalImageCount = await SiteImage.estimatedDocumentCount(); + return { images, totalImageCount }; + } + + async downloadImage (image, filename) { + const { minio: minioService } = this.dtp.services; + return minioService.downloadFile({ + bucket: image.file.bucket, + key: image.file.key, + filePath: filename, + }); + } + async deleteImage(image) { const { minio: minioService } = this.dtp.services; @@ -350,8 +371,8 @@ class ImageService extends SiteService { } module.exports = { - slug: 'image', - name: 'image', + logId: 'svc:image', + index: 'image', className: 'ImageService', create: (dtp) => { return new ImageService(dtp); }, }; \ No newline at end of file diff --git a/app/services/job-queue.js b/app/services/job-queue.js index b710bff..7018c1a 100644 --- a/app/services/job-queue.js +++ b/app/services/job-queue.js @@ -62,8 +62,8 @@ class JobQueueService extends SiteService { } module.exports = { - slug: 'job-queue', - name: 'jobQueue', + logId: 'svc:job-queue', + index: 'jobQueue', className: 'JobQueueService', create: (dtp) => { return new JobQueueService(dtp); }, }; \ No newline at end of file diff --git a/app/services/limiter.js b/app/services/limiter.js index 8b79db8..80ad38b 100644 --- a/app/services/limiter.js +++ b/app/services/limiter.js @@ -69,8 +69,8 @@ class LimiterService extends SiteService { } module.exports = { - slug: 'limiter', - name: 'limiter', + logId: 'svc:limiter', + index: 'limiter', className: 'LimiterService', create: (dtp) => { return new LimiterService(dtp); }, }; \ No newline at end of file diff --git a/app/services/log.js b/app/services/log.js index e03c501..e910b6d 100644 --- a/app/services/log.js +++ b/app/services/log.js @@ -26,8 +26,8 @@ class SystemLogService extends SiteService { return logs; } - async getComponentSlugs ( ) { - return await Log.distinct('component.slug'); + async getComponentIds ( ) { + return await Log.distinct('component.logId'); } async getTotalCount ( ) { @@ -38,8 +38,8 @@ class SystemLogService extends SiteService { } module.exports = { - slug: 'log', - name: 'log', + logId: 'svc:log', + index: 'log', className: 'SystemLogService', create: (dtp) => { return new SystemLogService(dtp); }, }; \ No newline at end of file diff --git a/app/services/logan.js b/app/services/logan.js index 94e78ef..87df2d6 100644 --- a/app/services/logan.js +++ b/app/services/logan.js @@ -4,9 +4,7 @@ 'use strict'; -const os = require('os'); - -const { SiteService, SiteError } = require('../../lib/site-lib'); +const { SiteService } = require('../../lib/site-lib'); class LoganService extends SiteService { @@ -16,64 +14,62 @@ class LoganService extends SiteService { async start ( ) { await super.start(); - } - - async sendRequestEvent (component, req, event) { - if (process.env.DTP_LOGAN !== 'enabled') { - return; - } - if (req.user) { - event.data = event.data || { }; - event.data.user = { - _id: req.user._id, - username: req.user.username, - }; - } - event.ip = req.ip; - return this.sendEvent(component, event); - } - - async sendEvent (component, event) { - if (process.env.DTP_LOGAN !== 'enabled') { - return; - } - try { - const loganScheme = process.env.DTP_LOGAN_SCHEME || 'http'; - const loganUrl = `${loganScheme}://${process.env.DTP_LOGAN_HOST}/api/event`; - event.host = os.hostname(); - event['component.slug'] = component.slug; - event['component.name'] = component.className || component.name; + const { LoganClient } = await import('dtp-logan-api'); - this.log[event.level]('sending Logan event', { event }); + this.log.info('creating Logan client'); + this.logan = new LoganClient(); - const payload = JSON.stringify(event); - const response = await fetch(loganUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': payload.length, - 'X-LogAn-Auth': process.env.DTP_LOGAN_API_KEY, + this.log.info('initializing Logan client'); + await this.logan.initialize({ + log: this.log, + api: { + enabled: process.env.DTP_LOGAN === 'enabled', + key: process.env.DTP_LOGAN_API_KEY, + scheme: process.env.DTP_LOGAN_SCHEME, + host: process.env.DTP_LOGAN_HOST, + }, + request: { + userField: 'user', + userIdField: '_id', + usernameField: 'username', + }, + flags: { + includeHostname: true, + includeClientIpAddress: true, + includeUser: true, + }, + queue: { + enabled: true, + name: process.env.DTP_LOGAN_QUEUE_NAME || 'logan', + redis: { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT, + username: process.env.REDIS_USERNAME, // requires Redis >= 6 + password: process.env.REDIS_PASSWORD, + keyPrefix: process.env.REDIS_PREFIX, + }, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: true, }, - body: payload, - }); + }, + }); + } - const json = await response.json(); - if (!json.success) { - throw new SiteError(500, json.message); - } + async sendRequestEvent (component, req, event) { + return this.logan.sendRequestEvent(component, req, event); + } - return json; - } catch (error) { - this.log.error('failed to send LOGAN event', { event, error }); - // fall through - } + async sendEvent (component, event) { + return this.logan.sendEvent(component, event); } } module.exports = { - slug: 'logan', - name: 'logan', + logId: 'svc:logan', + index: 'logan', className: 'LoganService', create: (dtp) => { return new LoganService(dtp); }, }; \ No newline at end of file diff --git a/app/services/markdown.js b/app/services/markdown.js index ee80cbf..92b7aeb 100644 --- a/app/services/markdown.js +++ b/app/services/markdown.js @@ -32,8 +32,8 @@ class MarkdownService extends SiteService { } module.exports = { - slug: 'markdown', - name: 'markdown', + logId: 'svc:markdown', + index: 'markdown', className: 'MarkdownService', create: (dtp) => { return new MarkdownService(dtp); }, }; \ No newline at end of file diff --git a/app/services/media.js b/app/services/media.js index 24cd7e1..67f4daa 100644 --- a/app/services/media.js +++ b/app/services/media.js @@ -94,8 +94,8 @@ class MediaService extends SiteService { } module.exports = { - slug: 'media', - name: 'media', + logId: 'svc:media', + index: 'media', className: 'MediaService', create: (dtp) => { return new MediaService(dtp); }, }; \ No newline at end of file diff --git a/app/services/minio.js b/app/services/minio.js index d386cb2..da9258d 100644 --- a/app/services/minio.js +++ b/app/services/minio.js @@ -24,7 +24,6 @@ class MinioService extends SiteService { accessKey: process.env.MINIO_ACCESS_KEY, secretKey: process.env.MINIO_SECRET_KEY, }; - this.log.debug('MinIO config', { minioConfig }); this.minio = new Minio.Client(minioConfig); } @@ -98,8 +97,8 @@ class MinioService extends SiteService { } module.exports = { - slug: 'minio', - name: 'minio', + logId: 'svc:minio', + index: 'minio', className: 'MinioService', create: (dtp) => { return new MinioService(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/services/newsletter.js b/app/services/newsletter.js index 1010718..d730ba3 100644 --- a/app/services/newsletter.js +++ b/app/services/newsletter.js @@ -117,8 +117,8 @@ class NewsletterService extends SiteService { } module.exports = { - slug: 'newsletter', - name: 'newsletter', + logId: 'svc:newsletter', + index: 'newsletter', className: 'NewsletterService', create: (dtp) => { return new NewsletterService(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/services/oauth2.js b/app/services/oauth2.js index 1857aa1..fad0c2c 100644 --- a/app/services/oauth2.js +++ b/app/services/oauth2.js @@ -470,8 +470,8 @@ class OAuth2Service extends SiteService { } module.exports = { - slug: 'oauth2', - name: 'oauth2', + logId: 'svc:oauth2', + index: 'oauth2', className: 'OAuth2Service', create: (dtp) => { return new OAuth2Service(dtp); }, }; \ No newline at end of file diff --git a/app/services/otp-auth.js b/app/services/otp-auth.js index 5679334..590b717 100644 --- a/app/services/otp-auth.js +++ b/app/services/otp-auth.js @@ -238,8 +238,8 @@ class OtpAuthService extends SiteService { } module.exports = { - slug: 'otp-auth', - name: 'otpAuth', + logId: 'svc:otp-auth', + index: 'otpAuth', className: 'OtpAuthService', create: (dtp) => { return new OtpAuthService(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/services/page.js b/app/services/page.js index 6bc33a0..3b797ce 100644 --- a/app/services/page.js +++ b/app/services/page.js @@ -7,7 +7,7 @@ const striptags = require('striptags'); const slug = require('slug'); -const { SiteService, SiteError, SiteAsync } = require('../../lib/site-lib'); +const { SiteService, SiteError } = require('../../lib/site-lib'); const mongoose = require('mongoose'); const ObjectId = mongoose.Types.ObjectId; @@ -180,7 +180,6 @@ class PageService extends SiteService { return this.getById(pageId); } - async isParentPage (page) { if (page) { page = [page]; @@ -204,9 +203,37 @@ class PageService extends SiteService { return pages; } - async deletePage (page) { - this.log.info('deleting page', { pageId: page._id }); + async deletePage (page, options) { + options = Object.assign({ updateCache: true }, options); + + this.log.info('deleting page', { pageId: page._id, options }); await Page.deleteOne({ _id: page._id }); + + if (options.updateCache) { + await this.cacheMainMenuPages(); + } + } + + async removeForAuthor (author) { + /* + * Execute the updates without page cache updates + */ + await Page + .find({ author: author._id }) + .cursor() + .eachAsync(async (page) => { + try { + await this.deletePage(page, { updateCache: false }); + } catch (error) { + this.log.error('failed to remove page for author', { error }); + // fall through + } + }); + + /* + * and update the page cache once, instead. + */ + await this.cacheMainMenuPages(); } createPageSlug (pageId, pageTitle) { @@ -217,90 +244,92 @@ class PageService extends SiteService { return `${pageSlug}-${pageId}`; } - async cacheMainMenuPages () { - try { - const pages = await Page - .find({ status: 'published' }) - .select('slug menu') - .populate({path: 'menu.parent'}) - .lean(); - + async cacheMainMenuPages ( ) { let mainMenu = []; - await SiteAsync.each(pages, async (page) => { - if (page.menu.parent) { - let parent = page.menu.parent; - if (parent.status === 'published') { - let parentPage = mainMenu.find(item => item.slug === parent.slug); - if (parentPage) { - let childPage = { - url: `/page/${page.slug}`, - slug: page.slug, - icon: page.menu.icon, - label: page.menu.label, - order: page.menu.order, - }; - parentPage.children.splice(childPage.order, 0, childPage); - } - else { - let parentPage = { - url: `/page/${parent.slug}`, - slug: parent.slug, - icon: parent.menu.icon, - label: parent.menu.label, - order: parent.menu.order, - children: [], - }; - let childPage = { - url: `/page/${page.slug}`, - slug: page.slug, - icon: page.menu.icon, - label: page.menu.label, - order: page.menu.order, - }; - parentPage.children.splice(childPage.order, 0, childPage); - mainMenu.splice(parentPage.order, 0, parentPage); + + try { + await Page + .find({ status: 'published' }) + .select('slug menu') + .populate({path: 'menu.parent'}) + .lean() + .cursor() + .eachAsync(async (page) => { + if (page.menu.parent) { + let parent = page.menu.parent; + if (parent.status === 'published') { + let parentPage = mainMenu.find(item => item.slug === parent.slug); + if (parentPage) { + let childPage = { + url: `/page/${page.slug}`, + slug: page.slug, + icon: page.menu.icon, + label: page.menu.label, + order: page.menu.order, + }; + parentPage.children.splice(childPage.order, 0, childPage); + } + else { + let parentPage = { + url: `/page/${parent.slug}`, + slug: parent.slug, + icon: parent.menu.icon, + label: parent.menu.label, + order: parent.menu.order, + children: [], + }; + let childPage = { + url: `/page/${page.slug}`, + slug: page.slug, + icon: page.menu.icon, + label: page.menu.label, + order: page.menu.order, + }; + parentPage.children.splice(childPage.order, 0, childPage); + mainMenu.splice(parentPage.order, 0, parentPage); + } + } else { + let menuPage = { + url: `/page/${page.slug}`, + slug: page.slug, + icon: page.menu.icon, + label: page.menu.label, + order: page.menu.order, + children: [], + }; + mainMenu.splice(menuPage.order, 0, menuPage); + } + } else { + let isPageInMenu = mainMenu.find(item => item.slug === page.slug); + + if (!isPageInMenu) { + let menuPage = { + url: `/page/${page.slug}`, + slug: page.slug, + icon: page.menu.icon, + label: page.menu.label, + order: page.menu.order, + children: [], + }; + mainMenu.push(menuPage); + } } - } else { - let menuPage = { - url: `/page/${page.slug}`, - slug: page.slug, - icon: page.menu.icon, - label: page.menu.label, - order: page.menu.order, - children: [], - }; - mainMenu.splice(menuPage.order, 0, menuPage); - } - } else { - let isPageInMenu = mainMenu.find(item => item.slug === page.slug); - - if (!isPageInMenu) { - let menuPage = { - url: `/page/${page.slug}`, - slug: page.slug, - icon: page.menu.icon, - label: page.menu.label, - order: page.menu.order, - children: [], - }; - mainMenu.push(menuPage); + }); + + /* + * Sort the menu data + */ + mainMenu.sort((a, b) => a.order - b.order); + for (const menu of mainMenu) { + if (menu.children) { + menu.children.sort((a, b) => a.order - b.order); } } - }); - - mainMenu.sort((a, b) => a.order - b.order); - - await SiteAsync.each(mainMenu, async (menu) => { - if (menu.children) { - menu.children.sort((a, b) => a.order - b.order); - } - }); - const deleteResponse = await this.dtp.services.cache.del("mainMenu"); - this.dtp.log.info(deleteResponse); - const storeResponse = await this.dtp.services.cache.setObject("mainMenu", mainMenu); - this.dtp.log.info(storeResponse); - // const getresp = await this.dtp.services.cache.getObject("mainMenu"); + /* + * Update the cache + */ + await this.dtp.services.cache.setObject("mainMenu", mainMenu); } catch (error) { this.dtp.log.error('failed to build page menu', { error }); } @@ -313,8 +342,8 @@ class PageService extends SiteService { module.exports = { - slug: 'page', - name: 'page', + logId: 'svc:page', + index: 'page', className: 'PageService', create: (dtp) => { return new PageService(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/services/phone.js b/app/services/phone.js index 923c1c1..3ae21c0 100644 --- a/app/services/phone.js +++ b/app/services/phone.js @@ -55,8 +55,8 @@ class PhoneService extends SiteService { } module.exports = { - slug: 'phone', - name: 'phone', + logId: 'svc:phone', + index: 'phone', className: 'PhoneService', create: (dtp) => { return new PhoneService(dtp); }, }; \ No newline at end of file diff --git a/app/services/post.js b/app/services/post.js index ff04efe..782750d 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -400,6 +400,23 @@ class PostService extends SiteService { await Post.deleteOne({ _id: post._id }); } + async removeForAuthor (author) { + await Post + .find({ + authorType: author.type, + author: author._id, + }) + .cursor() + .eachAsync(async (post) => { + try { + await this.deletePost(post); + } catch (error) { + this.log.error('failed to remove post for author', { error }); + // fall through + } + }); + } + createPostSlug (postId, postTitle) { if ((typeof postTitle !== 'string') || (postTitle.length < 1)) { throw new Error('Invalid input for making a post slug'); @@ -410,8 +427,8 @@ class PostService extends SiteService { } module.exports = { - slug: 'post', - name: 'post', + logId: 'svc:post', + index: 'post', className: 'PostService', create: (dtp) => { return new PostService(dtp); }, }; \ No newline at end of file diff --git a/app/services/resource.js b/app/services/resource.js index 80ce1e0..167d8c7 100644 --- a/app/services/resource.js +++ b/app/services/resource.js @@ -118,8 +118,8 @@ class ResourceService extends SiteService { } module.exports = { - slug: 'resource', - name: 'resource', + logId: 'svc:resource', + index: 'resource', className: 'ResourceService', create: (dtp) => { return new ResourceService(dtp); }, -}; +}; \ No newline at end of file diff --git a/app/services/session.js b/app/services/session.js index 785c8a5..fdbd3b0 100644 --- a/app/services/session.js +++ b/app/services/session.js @@ -18,7 +18,7 @@ class SessionService extends SiteService { async start ( ) { await super.start(); - this.log.info(`starting ${module.exports.name} service`); + passport.serializeUser(this.serializeUser.bind(this)); passport.deserializeUser(this.deserializeUser.bind(this)); } @@ -110,8 +110,8 @@ class SessionService extends SiteService { } module.exports = { - slug: 'session', - name: 'session', + logId: 'svc:session', + index: 'session', className: 'SessionService', create: (dtp) => { return new SessionService(dtp); }, }; \ No newline at end of file diff --git a/app/services/site-link.js b/app/services/site-link.js index c3e32b2..f4720a3 100644 --- a/app/services/site-link.js +++ b/app/services/site-link.js @@ -83,8 +83,8 @@ class SiteLinkService extends SiteService { } module.exports = { - slug: 'site-link', - name: 'siteLink', + logId: 'svc:site-link', + index: 'siteLink', className: 'SiteLinkService', create: (dtp) => { return new SiteLinkService(dtp); }, }; \ No newline at end of file diff --git a/app/services/sms.js b/app/services/sms.js index 93c26c3..8496839 100644 --- a/app/services/sms.js +++ b/app/services/sms.js @@ -18,7 +18,6 @@ class SmsService extends SiteService { async start ( ) { await super.start(); - this.log.info(`starting ${module.exports.name} service`); } async stop ( ) { @@ -48,8 +47,8 @@ class SmsService extends SiteService { } module.exports = { - slug: 'sms', - name: 'sms', + logId: 'svc:sms', + index: 'sms', className: 'SmsService', create: (dtp) => { return new SmsService(dtp); }, }; \ No newline at end of file diff --git a/app/services/sticker.js b/app/services/sticker.js index 581bb58..6d6fdd8 100644 --- a/app/services/sticker.js +++ b/app/services/sticker.js @@ -11,7 +11,7 @@ const mongoose = require('mongoose'); const Sticker = mongoose.model('Sticker'); const User = mongoose.model('User'); -const { SiteService, SiteError, SiteAsync } = require('../../lib/site-lib'); +const { SiteService, SiteError } = require('../../lib/site-lib'); const MAX_CHANNEL_STICKERS = 50; const MAX_USER_STICKERS = 10; @@ -144,18 +144,42 @@ class StickerService extends SiteService { await Sticker.updateOne({ _id: sticker._id }, { $set: { status } }); } + /** + * Fetch and populate an Array of Sticker documents matching the input slugs. + * + * The returned Array may be smaller than the input Array (or empty) if some + * or none of the stickers have been deleted or simply don't exist. + * + * @param {Array} slugs an Array of sticker slugs + * @returns Array of populated matching Sticker documents + */ async resolveStickerSlugs (slugs) { - const stickers = [ ]; - await SiteAsync.each(slugs, async (slug) => { - const sticker = await Sticker.findOne({ slug: slug }); - if (!sticker) { - return; - } - stickers.push(sticker); - }); + const stickers = await Sticker + .find({ slug: { $in: slugs } }) + .populate(this.populateSticker) + .lean(); return stickers; } + /** + * Convert an array of sticker slugs to an array of sticker _id values. This + * is used during chat message ingest where the whole populated sticker just + * isn't needed and is all discarded. + * + * The returned Array may be smaller than the input Array (or empty) if some + * or none of the stickers have been deleted or simply don't exist. + * + * @param {Array} slugs an Array of sticker slugs + * @returns an Array of sticker IDs + */ + async resolveStickerSlugIds (slugs) { + const stickers = await Sticker + .find({ slug: { $in: slugs } }) + .select('_id') + .lean(); + return stickers.map((sticker) => sticker._id); + } + async removeSticker (sticker) { const stickerId = sticker._id; this.log.info('creating sticker delete job', { stickerId }); @@ -208,8 +232,8 @@ class StickerService extends SiteService { } module.exports = { - slug: 'sticker', - name: 'sticker', + logId: 'svc:sticker', + index: 'sticker', className: 'StickerService', create: (dtp) => { return new StickerService(dtp); }, }; \ No newline at end of file diff --git a/app/services/user-notification.js b/app/services/user-notification.js index 3b7c100..d528137 100644 --- a/app/services/user-notification.js +++ b/app/services/user-notification.js @@ -135,8 +135,8 @@ class UserNotificationService extends SiteService { } module.exports = { - name: 'userNotification', - slug: 'user-notification', + logId: 'svc:user-notification', + index: 'userNotification', className: 'UserNotificationService', create: (dtp) => { return new UserNotificationService(dtp); }, }; \ No newline at end of file diff --git a/app/services/user.js b/app/services/user.js index c9e7b8c..176be2c 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -11,6 +11,7 @@ const mongoose = require('mongoose'); const User = mongoose.model('User'); const CoreUser = mongoose.model('CoreUser'); const UserBlock = mongoose.model('UserBlock'); +const UserArchive = mongoose.model('UserArchive'); const passport = require('passport'); const PassportLocal = require('passport-local'); @@ -49,13 +50,20 @@ class UserService extends SiteService { async start ( ) { await super.start(); - this.log.info(`starting ${module.exports.name} service`); this.registerPassportLocal(); - if (process.env.DTP_ADMIN === 'enabled') { this.registerPassportAdmin(); } + + const { jobQueue: jobQueueService } = this.dtp.services; + this.jobQueues = { }; + + this.log.info('connecting to job queue', { name: 'reeeper', config: this.dtp.config.jobQueues.reeeper }); + this.jobQueues.reeeper = jobQueueService.getJobQueue( + 'reeeper', + this.dtp.config.jobQueues.reeeper, + ); } async stop ( ) { @@ -71,6 +79,7 @@ class UserService extends SiteService { } = this.dtp.services; try { + this.checkRestrictedKeys('create', userDefinition); userDefinition.email = userDefinition.email.trim().toLowerCase(); // strip characters we don't want to allow in username @@ -215,6 +224,8 @@ class UserService extends SiteService { throw SiteError(403, 'Invalid user account operation'); } + this.checkRestrictedKeys('create', userDefinition); + userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, '')); const username_lc = userDefinition.username.toLowerCase(); @@ -606,6 +617,24 @@ class UserService extends SiteService { return users; } + async getAdmins ( ) { + const admins = await User + .find({ 'flags.isAdmin': true }) + .select(UserService.USER_SELECT) + .sort({ username: 1 }) + .lean(); + return admins; + } + + async getModerators ( ) { + const moderators = await User + .find({ 'flags.isModerator': true }) + .select(UserService.USER_SELECT) + .sort({ username: 1 }) + .lean(); + return moderators; + } + async setUserSettings (user, settings) { const { crypto: cryptoService, @@ -763,7 +792,18 @@ class UserService extends SiteService { const { image: imageService } = this.dtp.services; this.log.info('remove profile photo', { user: user._id }); - user = await this.getUserAccount(user._id); + switch (user.type) { + case 'User': + user = await this.getLocalUserAccount(user._id); + break; + + case 'CoreUser': + user = await this.getCoreUserAccount(user._id); + break; + + default: + throw new SiteError(400, 'Invalid User type'); + } if (user.picture.large) { await imageService.deleteImage(user.picture.large); } @@ -872,11 +912,14 @@ class UserService extends SiteService { async ban (user) { const { + attachment: attachmentService, chat: chatService, comment: commentService, contentReport: contentReportService, csrfToken: csrfTokenService, otpAuth: otpAuthService, + page: pageService, + post: postService, sticker: stickerService, userNotification: userNotificationService, } = this.dtp.services; @@ -893,12 +936,19 @@ class UserService extends SiteService { 'permissions.canChat': false, 'permissions.canComment': false, 'permissions.canReport': false, + 'permissions.canAuthorPages': false, + 'permissions.canAuthorPosts': false, + 'permissions.canPublishPages': false, + 'permissions.canPublishPosts': false, 'optIn.system': false, 'optIn.marketing': false, }, }, ); + await pageService.removeForAuthor(user); + await postService.removeForAuthor(user); + await chatService.removeForUser(user); await commentService.removeForAuthor(user); await contentReportService.removeForUser(user); @@ -906,12 +956,124 @@ class UserService extends SiteService { await otpAuthService.removeForUser(user); await stickerService.removeForUser(user); await userNotificationService.removeForUser(user); + await attachmentService.removeForOwner(user); + } + + checkRestrictedKeys (method, definition) { + const { logan: loganService } = this.dtp.services; + const restrictedKeys = [ + 'isAdmin', 'isModerator', 'isEmailVerified', + 'canLogin', 'canChat', 'canComment', 'canReport', + 'optInSystem', 'optInMarketing', + ]; + + const keys = Object.keys(definition); + for (const restrictedKey of restrictedKeys) { + if (keys.includes(restrictedKey)) { + loganService.sendEvent(module.exports, { + level: 'alert', + event: method, + message: 'malicious fields detected', + data: { definition }, + }); + throw new SiteError(403, 'invalid request'); + } + } + } + + /** + * Create a job to archive and ban a User (local). The job will immediately + * disable the specified user, create a .zip file of their content on storage. + * Once the worker confirms that the archive file is on storage, it creates a + * UserArchive record for it, then completely bans the User. That removes all + * of the User's content. + * + * It then removes the User record entirely. + * + * @param {User} user the User to be archived + * @returns the newly created Bull queue job + */ + async archiveLocalUser (user) { + return this.jobQueues.reeeper.add('archive-user-local', { userId: user._id }); + } + + /** + * Update a UserArchive document + * @param {UserArchive} archive the existing archive to be updated + * @param {*} archiveDefinition new values to be applied + */ + async updateArchive (archive, archiveDefinition) { + const update = { $set: { }, $unset: { } }; + + archiveDefinition.notes = archiveDefinition.notes.trim(); + if (archiveDefinition.notes && (archiveDefinition.notes.length > 0)) { + update.$set.notes = archiveDefinition.notes; + } else { + update.$unset.notes = 1; + } + + await UserArchive.updateOne({ _id: archive._id }, update); + } + + /** + * Fetch an Array of UserArchive documents with pagination. + * @param {DtpPagination} pagination self explanatory + * @returns Array of UserArchive documents (can be empty) + */ + async getArchives (pagination) { + const search = { }; + const archives = await UserArchive + .find(search) + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .lean(); + const totalArchiveCount = await UserArchive.estimatedDocumentCount(); + return { archives, totalArchiveCount }; + } + + /** + * Fetch a UserArchive record. This does not fetch the archive file. + * @param {UserArchive} archiveId the ID of the archive to fetch + * @returns the requested UserArchive, or null/undefined. + */ + async getArchiveById (archiveId) { + const archive = await UserArchive.findOne({ _id: archiveId }).lean(); + return archive; + } + + /** + * Removes the .zip file attached to a UserArchive. + * @param {UserArchive} archive the archive for which an associated .zip file + * is to be removed + */ + async deleteArchiveFile (archive) { + const { minio: minioService } = this.dtp.services; + if (!archive.archive || !archive.archive.bucket || !archive.archive.key) { + return; // no archive file present, abort + } + await minioService.removeObject(archive.archive.bucket, archive.archive.key); + await UserArchive.updateOne( + { _id: archive._id }, + { + $unset: { archive: 1 }, + }, + ); + } + + /** + * Removes a UserArchive and any attached data. + * @param {UserArchive} archive the UserArchive to be removed. + */ + async deleteArchive (archive) { + await this.deleteArchiveFile(archive); + await UserArchive.deleteOne({ _id: archive._id }); } } module.exports = { - slug: 'user', - name: 'user', + logId: 'svc:user', + index: 'user', className: 'UserService', create: (dtp) => { return new UserService(dtp); }, }; \ No newline at end of file diff --git a/app/services/venue.js b/app/services/venue.js index 47b6f25..bbc4de0 100644 --- a/app/services/venue.js +++ b/app/services/venue.js @@ -91,7 +91,8 @@ class VenueService extends SiteService { channel.description = status.description; await channel.save(); - await this.updateChannelStatus(channel); + + channel.currentStatus = status; return channel.toObject(); } @@ -105,7 +106,6 @@ class VenueService extends SiteService { updateOp.$set.slug = this.getChannelSlug(channelDefinition.url); updateOp.$set.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10); - const status = await this.updateChannelStatus(channel); updateOp.$set.name = status.name; updateOp.$set.description = status.description; @@ -120,7 +120,9 @@ class VenueService extends SiteService { updateOp.$set['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim(); channel = await VenueChannel.findOneAndUpdate({ _id: channel._id }, updateOp, { new: true }); - await this.updateChannelStatus(channel); + channel.currentStatus = await this.updateChannelStatus(channel); + + return channel; } async getChannels (pagination, options) { @@ -140,7 +142,7 @@ class VenueService extends SiteService { } const channels = await q.populate(this.populateVenueChannel).lean(); - for await (const channel of channels) { + for (const channel of channels) { channel.currentStatus = await this.updateChannelStatus(channel); } return channels; @@ -196,20 +198,31 @@ class VenueService extends SiteService { } async updateChannelStatus (channel) { - const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`; - this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl }); + const { logan: loganService } = this.dtp.services; + try { + const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`; + this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl }); + + const response = await fetch(requestUrl, { agent: this.httpsAgent }); + if (!response.ok) { + throw new SiteError(500, `Failed to fetch channel status: ${response.statusText}`); + } - const response = await fetch(requestUrl, { agent: this.httpsAgent }); - if (!response.ok) { - throw new SiteError(500, `Failed to fetch channel status: ${response.statusText}`); - } + const json = await response.json(); + if (!json.success) { + throw new Error(`failed to fetch channel status: ${json.message}`); + } - const json = await response.json(); - if (!json.success) { - throw new Error(`failed to fetch channel status: ${json.message}`); + return json.channel; + } catch (error) { + loganService.sendEvent(module.exports, { + level: 'error', + event: 'updateChannelStatus', + message: error.message, + data: { error }, + }); + return; // undefined } - - return json.channel; } getChannelSlug (channelUrl) { @@ -234,8 +247,8 @@ class VenueService extends SiteService { } module.exports = { - slug: 'venue', - name: 'venue', + logId: 'svc:venue', + index: 'venue', className: 'VenueService', create: (dtp) => { return new VenueService(dtp); }, }; \ No newline at end of file diff --git a/app/views/admin/attachment/index.pug b/app/views/admin/attachment/index.pug new file mode 100644 index 0000000..5120f13 --- /dev/null +++ b/app/views/admin/attachment/index.pug @@ -0,0 +1,21 @@ +extends ../layouts/main +block content + + h1 Attachments + + if Array.isArray(attachments) && (attachments.length > 0) + ul.uk-list.uk-list-divider + each attachment in attachments + li + div(uk-grid).uk-grid-small.uk-flex-middle + .uk-width-expand + //- had to abort while writing the renderer for an attachment. + //- will be back to finish this and have an attachment browser/manager. + pre= JSON.stringify(attachment, null, 2) + + .uk-width-auto + button(type="button", data-attachment-id= attachment._id, onclick="return dtp.adminApp.deleteAttachment(event);").uk-button.dtp-button-danger.uk-border-rounded + span + i.fas.fa-trash + else + div There are no attachments. \ No newline at end of file diff --git a/app/views/admin/comment/components/comment.pug b/app/views/admin/comment/components/comment.pug new file mode 100644 index 0000000..d480496 --- /dev/null +++ b/app/views/admin/comment/components/comment.pug @@ -0,0 +1,49 @@ +mixin renderComment (comment) + div(data-comment-id= comment._id).uk-card.uk-card-default.uk-card-small.dtp-site-comment.uk-border-rounded + .uk-card-header + div(uk-grid).uk-grid-medium.uk-flex-middle + .uk-width-auto + if comment.author.picture && comment.author.picture.small + img(src= `/image/${comment.author.picture.small._id}`).site-profile-picture.sb-small.uk-comment-avatar + else + img(src="/img/default-member.png").site-profile-picture.sb-small.uk-comment-avatar + + .uk-width-expand + h4.uk-comment-title.uk-margin-remove= comment.author.displayName || comment.author.username + .uk-comment-meta= moment(comment.created).fromNow() + + .uk-card-body + case comment.status + when 'published' + if comment.flags && comment.flags.isNSFW + div.uk-alert.uk-alert-info.uk-border-rounded + div(uk-grid).uk-grid-small.uk-text-small.uk-flex-middle + .uk-width-expand NSFW comment hidden by default. Use the eye to show/hide. + .uk-width-auto + button( + type="button", + uk-toggle={ target: `.comment-content[data-comment-id="${comment._id}"]` }, + title="Show/hide the comment text", + ).uk-button.uk-button-link + span + i.fas.fa-eye + .comment-content(data-comment-id= comment._id, hidden= comment.flags ? comment.flags.isNSFW : false)!= marked.parse(comment.content) + when 'removed' + .comment-content.uk-text-muted [comment removed] + when 'mod-warn' + alert + span A warning has been added to this comment. + button(type="button", uk-toggle={ target: `.comment-content[data-comment-id="${comment._id}"]` }) + .comment-content(data-comment-id= comment._id, hidden)!= marked.parse(comment.content) + when 'mod-removed' + .comment-content.uk-text-muted [comment removed] + + //- Comment meta bar + .uk-card-footer + div(uk-grid).uk-grid-small.uk-text-small.uk-text-muted + .uk-width-auto + +renderLabeledIcon('fa-chevron-up', formatCount(comment.resourceStats.upvoteCount)) + .uk-width-auto + +renderLabeledIcon('fa-chevron-down', formatCount(comment.resourceStats.downvoteCount)) + .uk-width-auto + +renderLabeledIcon('fa-comment', formatCount(comment.commentStats.replyCount)) \ No newline at end of file diff --git a/app/views/admin/components/menu.pug b/app/views/admin/components/menu.pug index 9573dbb..2f07774 100644 --- a/app/views/admin/components/menu.pug +++ b/app/views/admin/components/menu.pug @@ -32,6 +32,19 @@ ul(uk-nav).uk-nav-default span.nav-item-icon i.fas.fa-bullhorn span.uk-margin-small-left Announcements + + li(class={ 'uk-active': (adminView === 'attachment') }) + a(href="/admin/attachment") + span.nav-item-icon + i.fas.fa-file + span.uk-margin-small-left Attachments + + li(class={ 'uk-active': (adminView === 'image') }) + a(href="/admin/image") + span.nav-item-icon + i.fas.fa-image + span.uk-margin-small-left Images + li(class={ 'uk-active': (adminView === 'post') }) a(href="/admin/post") span.nav-item-icon @@ -97,13 +110,18 @@ ul(uk-nav).uk-nav-default i.fas.fa-user span.uk-margin-small-left Users + li(class={ 'uk-active': (adminView === 'user-archive') }) + a(href="/admin/user/archive") + span.nav-item-icon + i.fas.fa-file-archive + span.uk-margin-small-left User Archive + li(class={ 'uk-active': (adminView === 'content-report') }) a(href="/admin/content-report") span.nav-item-icon i.fas.fa-ban span.uk-margin-small-left Content Reports - li.uk-nav-divider li(class={ 'uk-active': (adminView === 'core-node') }) diff --git a/app/views/admin/image/archive-user.pug b/app/views/admin/image/archive-user.pug new file mode 100644 index 0000000..e52098b --- /dev/null +++ b/app/views/admin/image/archive-user.pug @@ -0,0 +1,28 @@ +extends ../layouts/main +block content + + include ../user/components/list-item + + form(method="POST", action=`/admin/user/local/${image.owner._id}/archive`).uk-form + input(type="hidden", name="userId", value= image.owner._id) + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h1.uk-card-title Archive Local User + .uk-card-body + p This action will pull all images from storage into an archive file, place the archive file on storage, delete all the image records and storage data, then ban the User. The archive is produced first because images would be deleted during the ban. So, the archive is made, then the user is banned. + + p These are the #{numeral(imageHistory.length).format('0,0')} most recent images uploaded by #{image.owner.username}. + + div(uk-grid) + each image in imageHistory + .uk-width-medium + .uk-margin-small(uk-lightbox) + a(href=`/image/${image._id}`, data-type="image", data-caption=`id: ${image._id}`) + div + img(src= `/image/${image._id}`).responsive + .uk-card-footer.uk-flex.uk-flex-middle + .uk-width-expand + +renderBackButton() + + .uk-width-auto + button(type="submit").uk-button.uk-button-danger.uk-border-rounded Archive User \ No newline at end of file diff --git a/app/views/admin/image/index.pug b/app/views/admin/image/index.pug new file mode 100644 index 0000000..03823ad --- /dev/null +++ b/app/views/admin/image/index.pug @@ -0,0 +1,46 @@ +extends ../layouts/main +block content + + include ../user/components/list-item + include ../../components/pagination-bar + + h1.uk-text-center Image Manager + + if Array.isArray(images.images) && (images.images.length > 0) + div(uk-grid).uk-flex-center + each image in images.images + .uk-width-medium + .uk-margin-small(uk-lightbox) + a(href=`/image/${image._id}`, data-type="image", data-caption=`id: ${image._id}`) + div + img(src= `/image/${image._id}`).responsive + + if image.owner + .uk-margin-small + +renderUserListItem(image.owner) + + .uk-margin-small.uk-text-center + button(type="button").uk-button.uk-button-default.uk-button-small.uk-border-rounded + span Image Menu + + div(uk-drop={ mode: 'click', pos: 'top-center' }).uk-card.uk-card-default.uk-card-small.uk-border-rounded + .uk-card-header + .uk-text-small.uk-text-muted.uk-text-center id:#{image._id} + .uk-card-body + ul.uk-nav.uk-dropdown-nav + li + a(href="#", data-image-id= image._id, onclick="dtp.adminApp.deleteImage(event);") + span + i.fas.fa-trash + span.uk-margin-small-left Delete image + if image.owner + li + a(href=`/admin/image/${image._id}/archive-user`).uk-text-truncate + span + i.fas.fa-file-archive + span.uk-margin-small-left Archive and ban #[span.uk-text-bold= image.owner.username] + + +renderPaginationBar('/admin/image', images.totalImageCount) + + else + .uk-text-center There are no images. \ No newline at end of file diff --git a/app/views/admin/index.pug b/app/views/admin/index.pug index f801a78..ef6aac7 100644 --- a/app/views/admin/index.pug +++ b/app/views/admin/index.pug @@ -2,6 +2,10 @@ extends layouts/main block content include ../venue/components/channel-grid + include user/components/list-item + include comment/components/comment + + include ../chat/components/message div(uk-grid) div(class="uk-width-1-1 uk-width-auto@m") @@ -30,6 +34,68 @@ block content if Array.isArray(channels) && (channels.length > 0) +renderVenueChannelGrid(channels) + .uk-margin + div(uk-grid) + div(class="uk-width-1-1 uk-width-1-3@l") + h3 Admins + if Array.isArray(admins) && (admins.length > 0) + ul.uk-list.uk-list-divider + each member in admins + li + +renderUserListItem(member) + else + div There are no system admins. + + h3 Moderators + if Array.isArray(moderators) && (moderators.length > 0) + ul.uk-list.uk-list-divider + each member in moderators + li + +renderUserListItem(member) + else + div There are no system-level moderators. + + h3 Recent Members + if Array.isArray(recentMembers) && (recentMembers.length > 0) + ul.uk-list.uk-list-divider + each member in recentMembers + li + +renderUserListItem(member) + else + div There are no recent members. + + div(class="uk-width-1-1 uk-width-1-3@l") + h3 Recent Chat + if Array.isArray(recentChat) && (recentChat.length > 0) + ul.uk-list.uk-list-divider + each message in recentChat + li + div(uk-grid).uk-grid-small + .uk-width-expand + +renderChatMessage(message, { fullWidth: true }) + .uk-width-auto + a(href=`/admin/user/local/${message.author._id}`, uk-tooltip={ title: 'Manage user account' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded + span + i.fas.fa-wrench + else + div There is no recent chat. + + div(class="uk-width-1-1 uk-width-1-3@l") + h3 Recent Comments + if Array.isArray(recentComments.comments) && (recentComments.comments.length > 0) + ul.uk-list.uk-list-divider + each comment in recentComments.comments + li + div(uk-grid).uk-grid-small + .uk-width-expand + +renderComment(comment) + .uk-width-auto + a(href=`/admin/user/local/${comment.author._id}`, uk-tooltip={ title: 'Manage user account' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded + span + i.fas.fa-wrench + else + div There are no recent comments. + block viewjs script(src="/chart.js/chart.min.js") script(src="/chartjs-adapter-moment/chartjs-adapter-moment.min.js") diff --git a/app/views/admin/log/index.pug b/app/views/admin/log/index.pug index 9f11170..cb81254 100644 --- a/app/views/admin/log/index.pug +++ b/app/views/admin/log/index.pug @@ -37,7 +37,7 @@ block content tr td= moment(log.created).format('YYYY-MM-DD hh:mm:ss.SSS') td= log.level - td= log.component.slug + td= log.component.logId td div= log.message if log.metadata diff --git a/app/views/admin/user/archive/confirm.pug b/app/views/admin/user/archive/confirm.pug new file mode 100644 index 0000000..9f83eaa --- /dev/null +++ b/app/views/admin/user/archive/confirm.pug @@ -0,0 +1,28 @@ +extends ../../layouts/main +block content + + include ../../user/components/list-item + + form(method="POST", action=`/admin/user/local/${userAccount._id}/archive`).uk-form + input(type="hidden", name="userId", value= userAccount._id) + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h1.uk-card-title + span + i.fas.fa-id-card + span.uk-margin-small-left Archive Local User + .uk-card-body + .uk-margin + +renderUserListItem(userAccount) + + .uk-margin + p This action will archive #{userAccount.displayName || userAccount.username}'s content to a .zip file, place the .zip file on storage, create a UserArchive record for this User account, ban this User account, and remove this User account from the database. + + p #{userAccount.displayName || userAccount.username}'s email address and username will become locked, and will remain unavailable for use for as long as this archive exists. + + .uk-card-footer.uk-flex.uk-flex-middle + .uk-width-expand + +renderBackButton() + + .uk-width-auto + button(type="submit").uk-button.uk-button-danger.uk-border-rounded Archive User \ No newline at end of file diff --git a/app/views/admin/user/archive/index.pug b/app/views/admin/user/archive/index.pug new file mode 100644 index 0000000..798399d --- /dev/null +++ b/app/views/admin/user/archive/index.pug @@ -0,0 +1,41 @@ +extends ../../layouts/main +block content + + include ../components/list-item + include ../../../components/pagination-bar + + .uk-card.uk-card-default.uk-card-small.uk-margin + .uk-card-header + h1.uk-card-title + span + i.fas.fa-id-card + span.uk-margin-small-left User Archives + + .uk-card-body + if Array.isArray(archive.archives) && (archive.archives.length > 0) + table.uk-table.uk-table-divider.uk-table-justify + thead + tr + th Username + th User ID + th Created + th Archive + tbody + each record in archive.archives + tr + td= record.user.username + td= record.user._id + td= moment(record.created).format('MMMM DD, YYYY, [at] h:mm a') + td + span + i.fas.fa-file-archive + a(href=`/admin/user/archive/${record._id}`).uk-margin-small-left View Archive + else + div There are no user archives. + + if Array.isArray(archive.archives) && (archive.archives.length > 0) + .uk-card-footer + +renderPaginationBar('/admin/user/archive', archive.totalArchiveCount) + + .uk-margin + .uk-text-small.uk-text-muted.uk-text-center User accounts referenced on this page have been removed from the database and are no longer able to use #{site.name}. \ No newline at end of file diff --git a/app/views/admin/user/archive/job.pug b/app/views/admin/user/archive/job.pug new file mode 100644 index 0000000..fbb33cc --- /dev/null +++ b/app/views/admin/user/archive/job.pug @@ -0,0 +1,7 @@ +extends ../../layouts/main +block content + + include ../components/list-item + + h1 User Archive Job + pre= JSON.stringify(job, null, 2) \ No newline at end of file diff --git a/app/views/admin/user/archive/view.pug b/app/views/admin/user/archive/view.pug new file mode 100644 index 0000000..a0122fe --- /dev/null +++ b/app/views/admin/user/archive/view.pug @@ -0,0 +1,83 @@ +extends ../../layouts/main +block content + + include ../components/list-item + + form(method="POST", action=`/admin/user/archive/${archive._id}/action`).uk-form + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h1.uk-card-title + span + i.fas.fa-id-card + span.uk-margin-small-left User Archive + + .uk-card-body + .uk-margin + div(uk-grid) + .uk-width-auto + .uk-form-label Archive ID + .uk-text-bold= archive._id + .uk-width-auto + .uk-form-label Created + .uk-text-bold= moment(archive.created).format('MMMM DD, YYYY, [at] h:mm:ss a') + .uk-width-auto + .uk-form-label User + .uk-text-bold= archive.user.username + .uk-width-auto + .uk-form-label User ID + .uk-text-bold= archive.user._id + .uk-width-auto + .uk-form-label User email + .uk-text-bold= archive.user.email + + if archive.archive + div(uk-grid) + .uk-width-auto + .uk-form-label Archive file + .uk-text-bold= archive.archive.key.replace(/\/user-archive\//, '') + .uk-width-auto + .uk-form-label Download size + .uk-text-bold= numeral(archive.archive.size).format('0,0.0a') + else + .uk-text-italic (archive file removed) + + .uk-margin + label(for="notes").uk-form-label Notes + textarea(id="notes", name="notes", rows="4", placeholder="Enter notes").uk-textarea.uk-resize-vertical= archive.notes + + .uk-card-footer + div(uk-grid) + .uk-width-expand + div(hidden= !archive.archive, uk-grid) + .uk-width-auto + a(href=`/admin/user/archive/${archive._id}/file`).uk-button.uk-button-default.uk-border-rounded + span + i.fas.fa-download + span.uk-margin-small-left Download#[span(class="uk-visible@s") File] + .uk-width-auto + button( + type="submit", + name="action", + value="delete-file", + uk-tooltip={ title: 'Remove the .zip file attached to the UserArchive' }, + ).uk-button.uk-button-danger.uk-border-rounded + span + i.fas.fa-trash + span.uk-margin-small-left Delete#[span(class="uk-visible@s") File] + + .uk-width-auto + button( + type="submit", + name="action", + value="delete", + uk-tooltip={ title: 'Remove the UserArchive from the database' }, + ).uk-button.uk-button-danger.uk-border-rounded + span + i.fas.fa-save + span.uk-margin-small-left Delete + + .uk-width-auto + button(type="submit", name="action", value="update").uk-button.uk-button-primary.uk-border-rounded + span + i.fas.fa-save + span.uk-margin-small-left Update \ No newline at end of file diff --git a/app/views/admin/user/components/list-item.pug b/app/views/admin/user/components/list-item.pug new file mode 100644 index 0000000..ebdd631 --- /dev/null +++ b/app/views/admin/user/components/list-item.pug @@ -0,0 +1,15 @@ +mixin renderUserListItem (user) + div(uk-grid).uk-grid-small + .uk-width-auto + +renderProfileIcon(user) + .uk-width-expand + .uk-text-bold(style="line-height: 1;").uk-text-truncate= user.displayName || user.username + .uk-text-small.uk-text-muted + a(href= getUserProfileUrl(user))= user.username + .uk-text-small.uk-text-truncate= user.bio + if user.created + .uk-text-small.uk-text-muted created #{moment(user.created).fromNow()} + .uk-width-auto + a(href=`/admin/user/local/${user._id}`, uk-tooltip={ title: 'Manage user account' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded + span + i.fas.fa-wrench \ No newline at end of file diff --git a/app/views/admin/user/form.pug b/app/views/admin/user/form.pug index 5d26940..e334d56 100644 --- a/app/views/admin/user/form.pug +++ b/app/views/admin/user/form.pug @@ -75,6 +75,7 @@ block content | Can Publish Posts .uk-margin + - userAccount.optIn = userAccount.optIn || { }; label.uk-form-label Opt-Ins div(uk-grid).uk-grid-small label @@ -93,6 +94,8 @@ block content div(uk-grid).uk-grid-small .uk-width-expand +renderBackButton() + .uk-width-auto + a(href=`/admin/user/local/${userAccount._id}/archive/confirm`).uk-button.uk-button-danger.uk-border-rounded Archive User .uk-width-auto button(type="submit", name="action", value="ban").uk-button.uk-button-danger.uk-border-rounded Ban User .uk-width-auto diff --git a/app/views/admin/user/index.pug b/app/views/admin/user/index.pug index 66c17c2..6afbfbc 100644 --- a/app/views/admin/user/index.pug +++ b/app/views/admin/user/index.pug @@ -3,6 +3,19 @@ block content include ../../components/pagination-bar + .uk-margin + div(uk-grid).uk-flex-middle + .uk-width-expand + h1 + span + i.fas.fa-user-cog + span.uk-margin-small-left User Manager + .uk-width-auto + a(href="/admin/user/archive").uk-button.uk-button-default.uk-border-rounded + span.nav-item-icon + i.fas.fa-file-archive + span.uk-margin-small-left Browse Archive + .uk-margin form(method="GET", action="/admin/user").uk-form div(uk-grid).uk-grid-collapse diff --git a/app/views/chat/components/message.pug b/app/views/chat/components/message.pug index ea5f39b..793800a 100644 --- a/app/views/chat/components/message.pug +++ b/app/views/chat/components/message.pug @@ -2,8 +2,10 @@ include ../../sticker/components/sticker mixin renderChatMessage (message, options = { }) - var authorName = message.author.displayName || message.author.username; div( - data-message-id= message._id, data-author-id= message.author._id - ).chat-message + data-message-id= message._id, + data-author-id= message.author._id, + class={ 'full-width': options.fullWidth }, + ).site-chat-message .uk-margin-small div(uk-grid).uk-grid-small .uk-width-auto @@ -34,7 +36,7 @@ mixin renderChatMessage (message, options = { }) //- "time" is filled in by the JavaScript client using the browser's locale //- information so that "time" is always in the user's display timezone. - .chat-timestamp(data-dtp-timestamp= message.created).uk-text-small + .chat-timestamp(data-dtp-timestamp= message.created, data-dtp-timestamp-format= 'datetime').uk-text-small.uk-text-muted if Array.isArray(message.stickers) && (message.stickers.length > 0) each sticker in message.stickers diff --git a/app/views/newsroom/index.pug b/app/views/newsroom/index.pug index dc8a8f5..14e9456 100644 --- a/app/views/newsroom/index.pug +++ b/app/views/newsroom/index.pug @@ -6,7 +6,14 @@ block content section.uk-section.uk-section-default.uk-section-small .uk-container - h1 #{site.name} Newsroom + .uk-margin + div(uk-grid).uk-flex-middle + .uk-width-expand + h1.uk-margin-remove #{site.name} Newsroom + .uk-width-auto + a(href="/newsroom/feed").uk-button.dtp-button-primary.uk-button-small.uk-border-rounded + span View All + if Array.isArray(newsroom.feeds) && (newsroom.feeds.length > 0) div(uk-grid).uk-grid-match each feed in newsroom.feeds diff --git a/app/views/venue/components/channel-list-item.pug b/app/views/venue/components/channel-list-item.pug index 4284180..6be6de6 100644 --- a/app/views/venue/components/channel-list-item.pug +++ b/app/views/venue/components/channel-list-item.pug @@ -15,7 +15,10 @@ mixin renderVenueChannelListItem (channel, options) .uk-width-expand.uk-text-truncate +renderUserLink(channel.owner) .uk-width-auto - if channel.currentStatus.status === 'live' - span.uk-text-success LIVE + if channel.currentStatus && channel.currentStatus.success + if channel.currentStatus.status === 'live' + span.uk-text-success LIVE + else + span= moment(channel.currentStatus.lastLive).fromNow() else - span= moment(channel.currentStatus.lastLive).fromNow() \ No newline at end of file + span --- \ No newline at end of file diff --git a/app/workers/chat.js b/app/workers/chat.js index 0b5a2e7..2e981ca 100644 --- a/app/workers/chat.js +++ b/app/workers/chat.js @@ -17,7 +17,7 @@ module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); module.config = { environment: process.env.NODE_ENV, root: module.rootPath, - component: { name: 'chatWorker', slug: 'chat-worker' }, + component: { logId: 'wrk:chat', index: 'chatWorker', className: 'ChatWorker' }, }; module.config.site = require(path.join(module.rootPath, 'config', 'site')); diff --git a/app/workers/chat/job/chat-room-clear.js b/app/workers/chat/job/chat-room-clear.js index 922deaf..2c43580 100644 --- a/app/workers/chat/job/chat-room-clear.js +++ b/app/workers/chat/job/chat-room-clear.js @@ -16,8 +16,9 @@ class ChatRoomClearJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'charRoomClearJob', - slug: 'chat-room-clear-job', + logId: 'wrk:chat:room-clear:job', + index: 'charRoomClearJob', + className: 'ChatRoomClearJob', }; } diff --git a/app/workers/chat/job/chat-room-delete.js b/app/workers/chat/job/chat-room-delete.js index 77998a2..37ca2e5 100644 --- a/app/workers/chat/job/chat-room-delete.js +++ b/app/workers/chat/job/chat-room-delete.js @@ -15,18 +15,13 @@ const EmojiReaction = mongoose.model('EmojiReaction'); const { SiteWorkerProcess } = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'site-lib')); -/** - * DTP Core Chat sticker processor can receive requests to ingest and delete - * stickers to be executed as background jobs in a queue. This processor - * attaches to the `media` queue and registers processors for `sticker-ingest` - * and `sticker-delete`. - */ class ChatRoomDeleteJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'chatRoomProcessor', - slug: 'chat-room-processor', + logId: 'wrk:chat:room-delete:job', + index: 'chatRoomDeleteJob', + className: 'ChatRoomDeleteJob', }; } diff --git a/app/workers/host-services.js b/app/workers/host-services.js index 8b2f9ab..d8766bd 100644 --- a/app/workers/host-services.js +++ b/app/workers/host-services.js @@ -32,9 +32,9 @@ module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); module.config = { environment: process.env.NODE_ENV, root: module.rootPath, - component: { name: 'hostServicesWorker', slug: 'host-services-worker' }, site: require(path.join(module.rootPath, 'config', 'site')), http: require(path.join(module.rootPath, 'config', 'http')), + component: { logId: 'wrk:host-services', index: 'hostServicesWorker', className: 'HostServicesWorker' }, }; module.log = new SiteLog(module, module.config.component); @@ -125,7 +125,7 @@ class HostCacheTransaction { constructor (dtp, message, rinfo) { this.dtp = dtp; this.created = Date.now(); // timestamp, not Date instance - this.component = { name: 'Host Cache Transaction', slug: 'host-cache-transaction' }; + this.component = { logId: 'host-cache-transaction', index: 'hostCacheTransaction', className: 'HostCacheTransaction' }; this.log = new SiteLog(dtp, this.component); this.message = message; @@ -238,7 +238,11 @@ class HostCacheTransaction { class TransactionManager { constructor (dtp) { this.dtp = dtp; - this.component = { name: 'Transaction Manager', slug: 'transaction-manager' }; + this.component = { + logId: 'transaction-manager', + index: 'transactionManager', + className: 'TransactionManager', + }; this.log = new SiteLog(dtp, this.component); this.transactions = { }; } diff --git a/app/workers/logan.js b/app/workers/logan.js new file mode 100644 index 0000000..65980ff --- /dev/null +++ b/app/workers/logan.js @@ -0,0 +1,92 @@ +'use strict'; + +require('dotenv').config(); + +const path = require('path'); + +const { + SiteLog, + SiteWorker, +} = require(path.join(__dirname, '..', '..', 'lib', 'site-lib')); + +module.rootPath = path.resolve(__dirname, '..', '..'); +module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); + +module.config = { + environment: process.env.NODE_ENV, + root: module.rootPath, + site: require(path.join(module.rootPath, 'config', 'site')), + component: { logId: 'wrk:logan-site', index: 'loganSite', className: 'LoganSiteWorker' }, +}; + +class LoganSiteWorker extends SiteWorker { + + constructor (dtp) { + super(dtp, dtp.config.component); + } + + async start ( ) { + await super.start(); + + const { LoganWorker } = await import('dtp-logan-api'); + + this.log.info('creating Logan worker'); + this.loganWorker = new LoganWorker(); + + this.log.info('initializing Logan worker'); + await this.loganWorker.initialize({ + api: { + enabled: process.env.DTP_LOGAN === 'enabled', + key: process.env.DTP_LOGAN_API_KEY, + scheme: process.env.DTP_LOGAN_SCHEME, + host: process.env.DTP_LOGAN_HOST, + }, + queue: { + enabled: true, + name: 'logan', + redis: { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT, + username: process.env.REDIS_USERNAME, // requires Redis >= 6 + password: process.env.REDIS_PASSWORD, + keyPrefix: process.env.REDIS_PREFIX, + }, + defaultJobOptions: { + attempts: 3, + priority: 10, + removeOnComplete: true, + removeOnFail: true, + }, + }, + }); + } + + async stop ( ) { + if (this.loganWorker) { + await this.loganWorker.close(); + delete this.loganWorker; + } + await super.stop(); + } +} + +(async ( ) => { + + module.log = new SiteLog(module, module.config.component); + + if (!process.env.DTP_LOGAN_API_KEY) { + console.log('Must define DTP_LOGAN_API_KEY environment variable to run test'); + process.exit(-1); + } + + try { + module.worker = new LoganSiteWorker(module); + await module.worker.start(); + + module.log.info(`${module.pkg.name} v${module.pkg.version} ${module.config.component.name} started`); + } catch (error) { + module.log.error('failed to start worker', { component: module.config.component, error }); + process.exit(-1); + } + +})(); \ No newline at end of file diff --git a/app/workers/media.js b/app/workers/media.js index 9906719..1f768e3 100644 --- a/app/workers/media.js +++ b/app/workers/media.js @@ -15,7 +15,7 @@ module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); module.config = { environment: process.env.NODE_ENV, root: path.resolve(__dirname, '..', '..'), - component: { name: 'mediaWorker', slug: 'media-worker' }, + component: { logId: 'wrk:media', index: 'media', className: 'MediaWorker' }, }; /** @@ -71,7 +71,7 @@ class MediaWorker extends SiteWorker { (async ( ) => { try { module.log = new SiteLog(module, module.config.component); - await SitePlatform.startPlatform(module, module.config.component); + await SitePlatform.start(module, module.config.component); module.worker = new MediaWorker(module); await module.worker.start(); diff --git a/app/workers/media/job/attachment-delete.js b/app/workers/media/job/attachment-delete.js index 50e52e8..9985898 100644 --- a/app/workers/media/job/attachment-delete.js +++ b/app/workers/media/job/attachment-delete.js @@ -15,8 +15,9 @@ class AttachmentDeleteJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'attachmentDeleteJob', - slug: 'attachment-delete-job', + logId: 'wrk:chat:attachment-delete:job', + index: 'attachmentDeleteJob', + className: 'AttachmentDeleteJob', }; } diff --git a/app/workers/media/job/attachment-ingest.js b/app/workers/media/job/attachment-ingest.js index 00d0aa6..1626ecf 100644 --- a/app/workers/media/job/attachment-ingest.js +++ b/app/workers/media/job/attachment-ingest.js @@ -19,8 +19,9 @@ class AttachmentIngestJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'attachmentIngestJob', - slug: 'attachment-ingest-job', + logId: 'wrk:chat:attachment-ingest:job', + index: 'attachmentIngestJob', + className: 'AttachmentIngestJob', }; } @@ -80,7 +81,7 @@ class AttachmentIngestJob extends SiteWorkerProcess { job.data.workPath = path.join( process.env.DTP_ATTACHMENT_WORK_PATH, - AttachmentIngestJob.COMPONENT.slug, + AttachmentIngestJob.COMPONENT.logId, attachment._id.toString(), ); diff --git a/app/workers/media/job/sticker-delete.js b/app/workers/media/job/sticker-delete.js index b4c642b..cb71f91 100644 --- a/app/workers/media/job/sticker-delete.js +++ b/app/workers/media/job/sticker-delete.js @@ -15,8 +15,9 @@ class StickerDeleteJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'stickerDeleteJob', - slug: 'sticker-delete-job', + logId: 'wrk:chat:sticker-delete:job', + index: 'stickerDeleteJob', + className: 'StickerDeleteJob', }; } diff --git a/app/workers/media/job/sticker-ingest.js b/app/workers/media/job/sticker-ingest.js index c4f6fcb..f127572 100644 --- a/app/workers/media/job/sticker-ingest.js +++ b/app/workers/media/job/sticker-ingest.js @@ -20,8 +20,9 @@ class StickerIngestJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'stickerIngestJob', - slug: 'sticker-ingest-job', + logId: 'wrk:chat:sticker-ingest:job', + index: 'stickerIngestJob', + className: 'StickerIngestJob', }; } @@ -74,7 +75,7 @@ class StickerIngestJob extends SiteWorkerProcess { job.data.workPath = path.join( process.env.DTP_STICKER_WORK_PATH, - this.dtp.config.component.slug, + this.dtp.config.component.logId, job.data.sticker._id.toString(), ); diff --git a/app/workers/media/job/webpage-screenshot.js b/app/workers/media/job/webpage-screenshot.js index a4a0bcd..7690629 100644 --- a/app/workers/media/job/webpage-screenshot.js +++ b/app/workers/media/job/webpage-screenshot.js @@ -17,8 +17,9 @@ class WebpageScreenshotJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'webpageScreenshotJob', - slug: 'webpage-screenshot-job', + logId: 'wrk:chat:webpage-screenshot:job', + index: 'webpageScreenshotJob', + className: 'WebpageScreenshotJob', }; } diff --git a/app/workers/newsletter.js b/app/workers/newsletter.js index 636e00c..2196c7a 100644 --- a/app/workers/newsletter.js +++ b/app/workers/newsletter.js @@ -11,8 +11,8 @@ const { SiteWorker, SiteLog } = require(path.join(__dirname, '..', '..', 'lib', module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); module.config = { - component: { name: 'newsletterWorker', slug: 'newsletter-worker' }, root: path.resolve(__dirname, '..', '..'), + component: { logId: 'wrk:newsletter', index: 'newsletter', className: 'NewsletterWorker' }, }; class NewsletterWorker extends SiteWorker { diff --git a/app/workers/newsletter/job/email-send.js b/app/workers/newsletter/job/email-send.js index d3f4072..fc8b63e 100644 --- a/app/workers/newsletter/job/email-send.js +++ b/app/workers/newsletter/job/email-send.js @@ -12,8 +12,9 @@ class NewsletterEmailSendJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'newsletterEmailSendJob', - slug: 'newsletter-email-send-job', + logId: 'wrk:newsletter:email-send:job', + index: 'newsletterEmailSendJob', + className: 'NewsletterEmailSendJob', }; } diff --git a/app/workers/newsletter/job/transmit.js b/app/workers/newsletter/job/transmit.js index 4990127..c8e4f1b 100644 --- a/app/workers/newsletter/job/transmit.js +++ b/app/workers/newsletter/job/transmit.js @@ -17,8 +17,9 @@ class NewsletterTransmitJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'newsletterTransmitJob', - slug: 'newsletter-transmit-job', + logId: 'wrk:newsletter:transmit:job', + index: 'newsletterTransmitJob', + className: 'NewsletterTransmitJob', }; } diff --git a/app/workers/newsroom.js b/app/workers/newsroom.js index 6ca4fdc..00e500a 100644 --- a/app/workers/newsroom.js +++ b/app/workers/newsroom.js @@ -9,6 +9,8 @@ const path = require('path'); require('dotenv').config({ path: path.resolve(__dirname, '..', '..', '.env') }); const mongoose = require('mongoose'); +const moment = require('moment'); + const { read: feedReader } = require('feed-reader'); const { @@ -23,7 +25,7 @@ module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); module.config = { environment: process.env.NODE_ENV, root: module.rootPath, - component: { name: 'newsroom', slug: 'newsroom' }, + component: { logId: 'wrk:newsroom', index: 'newsroom', className: 'NewsroomWorker' }, }; module.config.site = require(path.join(module.rootPath, 'config', 'site')); @@ -53,13 +55,32 @@ class NewsroomWorker extends SiteWorker { const NOW = new Date(); const { feed: feedService } = this.dtp.services; try { - this.log.info('loading latest feed data', { feedId: feed._id, title: feed.title }); + this.log.info('loading latest feed data and favicon', { + feed: { + _id: feed._id, + title: feed.title, + published: { + date: feed.published, + moment: moment(feed.published).fromNow(), + }, + }, + }); + + await feedService.updateFavicon(feed); const response = await feedReader(feed.url); + await SiteAsync.each(response.entries, async (entry) => { await Feed.updateOne({ _id: feed._id }, { $set: { published: feed.published || NOW }}); await feedService.createEntry(feed, entry); }, 4); - this.log.info('feed updated', { entries: response.entries.length }); + + this.log.info('feed updated', { + feed: { + _id: feed._id, + title: feed.title, + }, + entryCount: response.entries.length, + }); } catch (error) { this.log.error('failed to update feed', { feedId: feed._id, title: feed.title, error }); } diff --git a/app/workers/newsroom/cron/update-feeds.js b/app/workers/newsroom/cron/update-feeds.js index b4d6b5e..7f10e23 100644 --- a/app/workers/newsroom/cron/update-feeds.js +++ b/app/workers/newsroom/cron/update-feeds.js @@ -17,8 +17,9 @@ class UpdateFeedsCron extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'updateFeeds', - slug: 'update-feeds-cron', + logId: 'wrk:newsroom:update-feeds:cron', + index: 'updateFeeds', + className: 'UpdateFeedsCron', }; } diff --git a/app/workers/newsroom/job/update-feed.js b/app/workers/newsroom/job/update-feed.js index 6c96106..03c13a0 100644 --- a/app/workers/newsroom/job/update-feed.js +++ b/app/workers/newsroom/job/update-feed.js @@ -12,8 +12,9 @@ class UpdateFeedJob extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'newsroomUpdateFeedJob', - slug: 'newsroom-update-feed-job', + logId: 'wrk:newsroom:update-feed:job', + index: 'newsroomUpdateFeedJob', + className: 'UpdateFeedJob', }; } diff --git a/app/workers/reeeper.js b/app/workers/reeeper.js index 0236034..62bb2ad 100644 --- a/app/workers/reeeper.js +++ b/app/workers/reeeper.js @@ -19,7 +19,7 @@ module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); module.config = { environment: process.env.NODE_ENV, root: module.rootPath, - component: { name: 'reeeper', slug: 'reeeper' }, + component: { logId: 'wrk:reeeper', index: 'reeeper', className: 'ReeeperWorker' }, }; module.config.site = require(path.join(module.rootPath, 'config', 'site')); @@ -36,6 +36,8 @@ class ReeeperWorker extends SiteWorker { await this.loadProcessor(path.join(__dirname, 'reeeper', 'cron', 'expire-crashed-hosts.js')); await this.loadProcessor(path.join(__dirname, 'reeeper', 'cron', 'expire-announcements.js')); + await this.loadProcessor(path.join(__dirname, 'reeeper', 'job', 'archive-user-local.js')); + await this.startProcessors(); } diff --git a/app/workers/reeeper/cron/expire-announcements.js b/app/workers/reeeper/cron/expire-announcements.js index 648dec2..837830b 100644 --- a/app/workers/reeeper/cron/expire-announcements.js +++ b/app/workers/reeeper/cron/expire-announcements.js @@ -29,8 +29,9 @@ class ExpiredAnnouncementsCron extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'expiredAnnouncementsCron', - slug: 'expired-announcements-cron', + logId: 'wrk:chat:expired-announcements:cron', + index: 'expiredAnnouncementsCron', + className: 'ExpiredAnnouncementsCron', }; } diff --git a/app/workers/reeeper/cron/expire-crashed-hosts.js b/app/workers/reeeper/cron/expire-crashed-hosts.js index 2662fb2..17ea9a1 100644 --- a/app/workers/reeeper/cron/expire-crashed-hosts.js +++ b/app/workers/reeeper/cron/expire-crashed-hosts.js @@ -28,8 +28,9 @@ class CrashedHostsCron extends SiteWorkerProcess { static get COMPONENT ( ) { return { - name: 'crashedHostsCron', - slug: 'crashed-hosts-cron', + logId: 'wrk:reeeper:crashed-hosts:cron', + index: 'crashedHostsCron', + className: 'CrashedHostsCron', }; } diff --git a/app/workers/reeeper/job/archive-user-local.js b/app/workers/reeeper/job/archive-user-local.js new file mode 100644 index 0000000..d34bf3e --- /dev/null +++ b/app/workers/reeeper/job/archive-user-local.js @@ -0,0 +1,477 @@ +// reeeper/job/archive-user-local.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +const util = require('util'); +const execFile = util.promisify(require('child_process').execFile); + +const mime = require('mime'); + +const mongoose = require('mongoose'); +const User = mongoose.model('User'); + +const { SiteWorkerProcess } = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'site-lib')); + +/** + * A job to archive and ban a User (local). + * + * 1. Immediately disable the specified User + * 2. Create a .zip file of the User's content on storage + * 3. Creates a UserArchive record for the file and User + * 4. Ban the User (removes all of the User's content) + * 5. Remove the User record from the database + */ +class ArchiveUserLocalJob extends SiteWorkerProcess { + + static get COMPONENT ( ) { + return { + logId: 'wrk:reeeper:archive-user-local:job', + index: 'archiveUserLocalJob', + className: 'ArchiveUserLocalJob', + }; + } + + constructor (worker) { + super(worker, ArchiveUserLocalJob.COMPONENT); + this.jobs = new Set(); + } + + async start ( ) { + await super.start(); + + this.queue = await this.getJobQueue('reeeper', this.dtp.config.jobQueues.reeeper); + + this.log.info('registering job processor', { queue: this.queue.name }); + this.queue.process('archive-user-local', 1, this.processArchiveUserLocal.bind(this)); + } + + async stop ( ) { + try { + if (this.queue) { + this.log.info('halting job queue', { jobCount: this.jobs.size }); + await this.queue.pause(true, false); + delete this.queue; + } + } catch (error) { + this.log.error('failed to halt job queue', { error }); + // fall through + } finally { + await super.stop(); + } + } + + async processArchiveUserLocal (job) { + const { user: userService } = this.dtp.services; + try { + job.data.archivePath = path.join('/tmp', this.dtp.pkg.name, 'archive-user-local'); + this.jobs.add(job); + + job.data.userId = mongoose.Types.ObjectId(job.data.userId); + job.data.user = await userService.getLocalUserAccount(job.data.userId); + + job.data.workPath = path.join(job.data.archivePath, job.data.userId.toString()); + await fs.promises.mkdir(job.data.workPath, { recursive: true }); + + /* + * Save the User account data + */ + await this.archiveUserData(job); + + /* + * Disable the User account (which destroys their session and cookie(s)) + */ + await this.disableUser(job); + + /* + * Archive the User's content to the workPath on the local file system. + */ + await this.archiveUserChat(job); + await this.archiveUserComments(job); + await this.archiveUserStickers(job); + await this.archiveUserImages(job); + await this.archiveUserAttachments(job); + + /* + * Archive DTP Sites specific content + */ + await this.archiveUserPosts(job); + + /* + * Create the .zip file archive, upload it to storage, and create the + * UserArchive record. + */ + await this.createArchiveFile(job); + + this.log.info('banning user', { + user: { + _id: job.data.userId, + username: job.data.user.username, + }, + }); + await userService.ban(job.data.user); + + this.log.info('removing user', { + user: { + _id: job.data.userId, + username: job.data.user.username, + }, + }); + await User.deleteOne({ _id: job.data.userId }); + } catch (error) { + this.log.error('failed to archive user', { userId: job.data.userId, error }); + throw error; + } finally { + if (job.data.workPath) { + this.log.info('cleaning up work directory'); + await fs.promises.rm(job.data.workPath, { force: true, recursive: true }); + + delete job.data.workPath; + } + this.jobs.delete(job); + this.log.info('job complete', { job: job.id }); + } + } + + async archiveUserData (job) { + // fetch the entire User record (all fields) + job.data.fullUser = await User + .findOne({ _id: job.data.user._id }) + .select('+email +passwordSalt +password +flags +permissions +optIn') + .lean(); + if (!job.data.fullUser) { + throw new Error('user does not exist'); + } + + const userFilename = path.join(job.data.workPath, `user-${job.data.user._id}.json`); + await fs.promises.writeFile(userFilename, JSON.stringify(job.data.fullUser, null, 2)); + } + + async disableUser (job) { + this.log.info('disabling local User account', { + user: { + _id: job.data.userId, + username: job.data.user.username, + }, + }); + await User.updateOne( + { _id: job.data.user._id }, + { + $set: { + 'flags.isAdmin': false, + 'flags.isModerator': false, + 'flags.isEmailVerified': false, + 'permissions.canLogin': false, + 'permissions.canChat': false, + 'permissions.canComment': false, + 'permissions.canReport': false, + 'optIn.system': false, + 'optIn.marketing': false, + }, + }, + ); + } + + async archiveUserChat (job) { + const ChatMessage = mongoose.model('ChatMessage'); + const ChatRoom = mongoose.model('ChatRoom'); + + job.data.chatPath = path.join(job.data.workPath, 'chat'); + await fs.promises.mkdir(job.data.chatPath, { recursive: true }); + + this.log.info('archiving user chat', { + user: { + _id: job.data.user._id, + username: job.data.user.username, + }, + }); + + await ChatRoom + .find({ owner: job.data.user._id }) + + .lean() + .cursor() + .eachAsync(async (room) => { + const roomFilename = path.join(job.data.workPath, 'chat', `room-${room._id}`); + await fs.promises.writeFile(roomFilename, JSON.stringify(room, null, 2)); + }); + + await ChatMessage + .find({ author: job.data.user._id }) + .lean() + .cursor() + .eachAsync(async (message) => { + const messageFilename = path.join(job.data.workPath, 'chat', `message-${message._id}.json`); + await fs.promises.writeFile(messageFilename, JSON.stringify(message, null, 2)); + }); + } + + async archiveUserComments (job) { + const Comment = mongoose.model('Comment'); + + job.data.commentPath = path.join(job.data.workPath, 'comments'); + await fs.promises.mkdir(job.data.commentPath, { recursive: true }); + + this.log.info('archiving user comments', { + user: { + _id: job.data.user._id, + username: job.data.user.username, + }, + }); + + await Comment + .find({ author: job.data.userId }) + .cursor() + .eachAsync(async (comment) => { + const commentFilename = path.join(job.data.commentPath, `comment-${comment._id}.json`); + await fs.promises.writeFile(commentFilename, JSON.stringify(comment, null, 2)); + }); + } + + async archiveUserStickers (job) { + const Sticker = mongoose.model('Sticker'); + const { minio: minioService } = this.dtp.services; + + job.data.stickerPath = path.join(job.data.workPath, 'stickers'); + await fs.promises.mkdir(job.data.stickerPath, { recursive: true }); + + job.data.stickerMediaPath = path.join(job.data.stickerPath, 'media'); + await fs.promises.mkdir(job.data.stickerMediaPath, { recursive: true }); + + this.log.info('archiving user stickers', { + user: { + _id: job.data.user._id, + username: job.data.user.username, + }, + }); + + await Sticker + .find({ owner: job.data.userId }) + .cursor() + .eachAsync(async (sticker) => { + const stickerFilename = path.join(job.data.stickerPath, `sticker-${sticker._id}.json`); + await fs.promises.writeFile(stickerFilename, JSON.stringify(sticker, null, 2)); + + if (sticker.original && sticker.original.bucket && sticker.orignal.key && sticker.encoded.type) { + const originalExt = mime.getExtension(sticker.original.type); + const originalFilename = path.join(job.data.stickerMediaPath, `sticker-${sticker._id}.original.${originalExt}`); + await minioService.downloadFile({ + bucket: sticker.original.bucket, + key: sticker.original.key, + filePath: originalFilename, + }); + } + + if (sticker.encoded && sticker.encoded.bucket && sticker.encoded.key && sticker.encoded.type) { + const encodedExt = mime.getExtension(sticker.encoded.type); + const encodedFilename = path.join(job.data.stickerMediaPath, `sticker-${sticker._id}.encoded.${encodedExt}`); + await minioService.downloadFile({ + bucket: sticker.encoded.bucket, + key: sticker.encoded.key, + filePath: encodedFilename, + }); + } + }); + } + + async archiveUserImages (job) { + const SiteImage = mongoose.model('Image'); + const { image: imageService } = this.dtp.services; + + job.data.imagePath = path.join(job.data.workPath, 'images'); + await fs.promises.mkdir(job.data.imagePath, { recursive: true }); + + this.log.info('archiving user images', { + user: { + _id: job.data.user._id, + username: job.data.user.username, + }, + }); + + await SiteImage + .find({ owner: job.data.user._id }) + .cursor() + .eachAsync(async (image) => { + try { + let imageExt = mime.getExtension(image.type); + const imageFilename = path.join(job.data.imagePath, `image-${image._id}.${imageExt}`); + const metadataFilename = path.join(job.data.imagePath, `image-${image._id}.metadata.json`); + + await imageService.downloadImage(image, imageFilename); + await fs.promises.writeFile(metadataFilename, JSON.stringify(image.metadata, null, 2)); + + } catch (error) { + this.log.error('failed to download image', { + image: { _id: image._id }, + error, + }); + } + }); + } + + async archiveUserPosts (job) { + const Post = mongoose.model('Post'); + + job.data.postPath = path.join(job.data.workPath, 'posts'); + await fs.promises.mkdir(job.data.postPath, { recursive: true }); + + this.log.info('archiving user blog posts', { + user: { + _id: job.data.user._id, + username: job.data.user.username, + }, + }); + + await Post + .find({ author: job.data.user._id }) + .cursor() + .eachAsync(async (post) => { + const postFilename = path.join(job.data.postPath, `post-${post._id}.json`); + try { + await fs.promises.writeFile(postFilename, JSON.stringify(post, null, 2)); + } catch (error) { + this.log.error('failed to write user blog post file', { post: { _id: post._id }, error }); + // fall through + } + }); + } + + async archiveUserAttachments (job) { + const Attachment = mongoose.model('Attachment'); + const { minio: minioService } = this.dtp.services; + + job.data.attachmentPath = path.join(job.data.workPath, 'attachments'); + await fs.promises.mkdir(job.data.attachmentPath, { recursive: true }); + + job.data.originalAttachmentPath = path.join(job.data.attachmentPath, 'original'); + await fs.promises.mkdir(job.data.originalAttachmentPath, { recursive: true }); + + job.data.encodedAttachmentPath = path.join(job.data.attachmentPath, 'encoded'); + await fs.promises.mkdir(job.data.encodedAttachmentPath, { recursive: true }); + + this.log.info('archiving user attachments', { + user: { + _id: job.data.user._id, + username: job.data.user.username, + }, + }); + + await Attachment + .find({ owner: job.data.user._id }) + .cursor() + .eachAsync(async (attachment) => { + try { + /* + * Write the JSON record archive + */ + const metadataFilename = path.join(job.data.attachmentPath, `attachment-${attachment._id}.metadata.json`); + await fs.promises.writeFile(metadataFilename, JSON.stringify(attachment, null, 2)); + + /* + * Download and save the original file (if present) + */ + if (attachment.original && attachment.original.bucket && attachment.original.key) { + let originalExt = mime.getExtension(attachment.original.mime); + const originalFilename = path.join(job.data.originalAttachmentPath, `attachment-${attachment._id}.${originalExt}`); + await minioService.downloadFile({ + bucket: attachment.original.bucket, + key: attachment.original.key, + filePath: originalFilename, + }); + } + + /* + * Download and save the encoded file (if present) + */ + if (attachment.encoded && attachment.encoded.bucket && attachment.encoded.key) { + let encodedExt = mime.getExtension(attachment.encoded.mime); + const encodedFilename = path.join(job.data.encodedAttachmentPath, `attachment-${attachment._id}.${encodedExt}`); + await minioService.downloadFile({ + bucket: attachment.encoded.bucket, + key: attachment.encoded.key, + filePath: encodedFilename, + }); + } + } catch (error) { + this.log.error('failed to archive attachment', { + attachment: { _id: attachment._id }, + error, + }); + } + }); + } + + async createArchiveFile (job) { + const { minio: minioService } = this.dtp.services; + try { + job.data.zipFilename = path.join(job.data.archivePath, `user-${job.data.userId}.zip`); + const zipArgs = [ + '-r', '-9', + job.data.zipFilename, + `${job.data.userId}`, + ]; + const options = { + cwd: job.data.archivePath, + encoding: 'utf8', + }; + await execFile('/usr/bin/zip', zipArgs, options); + + const zipFileStat = await fs.promises.stat(job.data.zipFilename); + this.log.info('zip archive created', { size: zipFileStat.size }); + + job.data.archiveFile = { + bucket: process.env.MINIO_ADMIN_BUCKET, + key: `/user-archive/user-${job.data.userId}.zip`, + }; + + const response = await minioService.uploadFile({ + bucket: job.data.archiveFile.bucket, + key: job.data.archiveFile.key, + filePath: job.data.zipFilename, + metadata: { + job: { + id: job.id, + }, + user: job.data.user, + } + }); + + this.log.info('creating user archive record', { etag: response.etag, size: zipFileStat.size }); + const UserArchive = mongoose.model('UserArchive'); + await UserArchive.create({ + created: job.data.startTime, + user: { + _id: job.data.userId, + username: job.data.user.username, + email: job.data.fullUser.email, + }, + archive: { + bucket: job.data.archiveFile.bucket, + key: job.data.archiveFile.key, + etag: response.etag, + size: zipFileStat.size, + } + }); + } catch (error) { + this.log.error('failed to create archive .zip file', { + user: { + _id: job.data.userId, + username: job.data.user.username, + }, + }); + throw error; + } finally { + try { + await fs.promises.rm(job.data.zipFilename, { force: true }); + } catch (error) { + this.log.error('failed to remove temp .zip file', { error }); + } + } + } +} + +module.exports = ArchiveUserLocalJob; \ No newline at end of file diff --git a/client/js/index-admin.js b/client/js/index-admin.js index 6324dd1..48aed4d 100644 --- a/client/js/index-admin.js +++ b/client/js/index-admin.js @@ -4,7 +4,7 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Site Admin', slug: 'site-admin' }; +const DTP_COMPONENT = { logId: 'site-admin-index', index: 'siteAdminIndex', className: 'SiteAdminIndex' }; const dtp = window.dtp = window.dtp || { }; dtp.admin = dtp.admin || { }; @@ -13,11 +13,9 @@ import DtpSiteAdminApp from './site-admin-app.js'; import DtpWebLog from 'dtp/dtp-log.js'; window.addEventListener('load', async ( ) => { - // application console log dtp.admin.log = new DtpWebLog(DTP_COMPONENT); - dtp.adminApp = new DtpSiteAdminApp(dtp.user); - dtp.admin.log.debug('load', 'dispatching load event'); + dtp.admin.log.debug('load', 'dispatching admin load event'); window.dispatchEvent(new Event('dtp-load-admin')); }); \ No newline at end of file diff --git a/client/js/index.js b/client/js/index.js index cb321bc..9f2f661 100644 --- a/client/js/index.js +++ b/client/js/index.js @@ -4,13 +4,12 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Site', slug: 'site' }; +const DTP_COMPONENT = { logId: 'site-index', index: 'siteIndex', className: 'SiteIndex' }; const dtp = window. dtp = window.dtp || { }; import DtpSiteApp from './site-app.js'; import DtpWebLog from 'dtp/dtp-log.js'; -// import UIkit from 'uikit'; /** * Monkeypatch to count characters instead of .length's code point count. diff --git a/client/js/site-admin-app.js b/client/js/site-admin-app.js index 738fed7..583cd7b 100644 --- a/client/js/site-admin-app.js +++ b/client/js/site-admin-app.js @@ -4,7 +4,6 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Site Admin', slug: 'site-admin-app' }; const dtp = window.dtp = window.dtp || { }; const GRID_COLOR = '#a0a0a0'; @@ -25,10 +24,12 @@ import numeral from 'numeral'; import UIkit from 'uikit'; // import UIkit from 'uikit'; -export default class DtpSiteAdminHostStatsApp extends DtpApp { +export default class DtpSiteAdminApp extends DtpApp { + + static get COMPONENT ( ) { return { logId: 'site-admin-app', index: 'siteAdminApp', className: 'SiteAdminApp' }; } constructor (user) { - super(DTP_COMPONENT, user); + super(DtpSiteAdminApp.COMPONENT, user); this.log.debug('constructor', 'app instance created'); } @@ -528,6 +529,12 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { return; } + async deleteImage (event) { + const target = event.currentTarget || event.target; + const imageId = target.getAttribute('data-image-id'); + const response = await fetch(`/admin/image/${imageId}`, { method: 'DELETE' }); + return this.processResponse(response); + } } -dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp; \ No newline at end of file +dtp.DtpSiteAdminApp = DtpSiteAdminApp; \ No newline at end of file diff --git a/client/js/site-app.js b/client/js/site-app.js index 2f2289a..38ea858 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -4,7 +4,7 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Site App', slug: 'site-app' }; +const DTP_COMPONENT = { logId: 'site-app', index: 'siteApp', className: 'DtpSiteApp' }; const dtp = window.dtp = window.dtp || { }; import DtpApp from 'dtp/dtp-app.js'; diff --git a/client/js/site-chat.js b/client/js/site-chat.js index 56c7a8f..00a5874 100644 --- a/client/js/site-chat.js +++ b/client/js/site-chat.js @@ -4,7 +4,6 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Site Chat', slug: 'site-chat' }; const dtp = window.dtp = window.dtp || { }; // jshint ignore:line const EMOJI_EXPLOSION_DURATION = 8000; @@ -17,9 +16,11 @@ import * as picmo from 'picmo'; export default class SiteChat { + static get COMPONENT ( ) { return { logId: 'site-chat', index: 'siteChat', className: 'DtpSiteChat' }; } + constructor (app) { this.app = app; - this.log = new DtpLog(DTP_COMPONENT); + this.log = new DtpLog(SiteChat.COMPONENT); this.ui = { menu: document.querySelector('#chat-room-menu'), @@ -35,7 +36,6 @@ export default class SiteChat { if (this.ui.messageList) { this.ui.messageList.addEventListener('scroll', this.onChatMessageListScroll.bind(this)); - this.updateTimestamps(); setTimeout(( ) => { this.log.info('constructor', 'scrolling chat', { top: this.ui.messageList.scrollHeight }); this.ui.messageList.scrollTo({ top: this.ui.messageList.scrollHeight, behavior: 'instant' }); @@ -182,7 +182,7 @@ export default class SiteChat { this.ui.isModifying = true; this.ui.messageList.insertAdjacentHTML('beforeend', message.html); this.trimMessages(); - this.updateTimestamps(); + this.app.updateTimestamps(); if (isAtBottom) { /* @@ -223,7 +223,7 @@ export default class SiteChat { this.ui.messageList.appendChild(systemMessage); this.trimMessages(); - this.updateTimestamps(); + this.app.updateTimestamps(); if (this.ui.isAtBottom) { this.ui.messageList.scrollTo(0, this.ui.messageList.scrollHeight); @@ -236,15 +236,6 @@ export default class SiteChat { } } - updateTimestamps ( ) { - const timestamps = document.querySelectorAll('[data-dtp-timestamp]'); - timestamps.forEach((timestamp) => { - const created = timestamp.getAttribute('data-dtp-timestamp'); - const format = timestamp.getAttribute('data-dtp-time-format'); - timestamp.textContent = moment(created).format(format || 'MMM DD, YYYY, [at] hh:mm:ss a'); - }); - } - createEmojiReact (message) { this.ui.reactions.create(message.reaction); } diff --git a/client/js/site-comments.js b/client/js/site-comments.js index fe12dc3..e99b94c 100644 --- a/client/js/site-comments.js +++ b/client/js/site-comments.js @@ -14,7 +14,7 @@ export default class SiteComments { constructor (app) { this.app = app; - this.log = new DtpLog({ name: 'Site Comments', slug: 'comments' }); + this.log = new DtpLog({ logId: 'site-comments', index: 'siteComments', className: 'SiteComments' }); this.createEmojiPickers(); } diff --git a/client/js/site-reactions.js b/client/js/site-reactions.js index a6116fb..0ed58ea 100644 --- a/client/js/site-reactions.js +++ b/client/js/site-reactions.js @@ -4,13 +4,14 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Site Reactions', slug: 'site-reactions' }; const dtp = window.dtp = window.dtp || { }; // jshint ignore:line import DtpLog from 'dtp/dtp-log'; class Reaction { + static get COMPONENT ( ) { return { logId: 'reaction', index: 'reaction', className: 'Reaction' }; } + constructor (container, reaction) { this.container = container; this.reaction = reaction; @@ -96,8 +97,10 @@ class Reaction { export default class SiteReactions { + static get COMPONENT ( ) { return { logId: 'site-reactions', index: 'siteReactions', className: 'SiteReactions' }; } + constructor ( ) { - this.log = new DtpLog(DTP_COMPONENT); + this.log = new DtpLog(SiteReactions.COMPONENT); this.container = document.querySelector('#chat-reactions'); this.reactions = [ ]; diff --git a/client/less/site/chat.less b/client/less/site/chat.less index b227219..5863c73 100644 --- a/client/less/site/chat.less +++ b/client/less/site/chat.less @@ -13,6 +13,97 @@ background-color: @content-background-color; } +.site-chat-message { + padding: @grid-small-gutter-vertical @grid-small-gutter-horizontal; + margin: (@grid-small-gutter-vertical / 2) @grid-small-gutter-horizontal; + + border: solid 1px @content-border-color; + border-radius: 8px; + + background: @content-background-color; + color: inherit; + font-size: var(--dtp-chat-font-size); + + &.full-width { + margin-left: 0; + margin-right: 0; + + &:first-of-type { margin-top: 0; } + + &:last-of-type { margin-bottom: 0; } + } + + &.system-message { + background: #e8e8e8; + color: #1a1a1a; + + &[data-message-type="info"] { + background: #068be4; + color: white; + } + &[data-message-type="warning"] { + background: #e4c306; + color: white; + } + &[data-message-type="error"] { + background: #ff00131a; + color: white; + } + } + + .chat-username { + font-weight: bold; + font-size: var(--dtp-chat-font-size); + line-height: 1; + color: var(--dtp-chat-username-color); + } + + img.chat-author-image { + width: auto; + height: 40px; + border-radius: 4px; + } + + .chat-content { + line-height: 1.2em; + font-size: var(--dtp-chat-font-size); + color: inherit; + overflow-wrap: break-word; + + p:last-child { + margin-bottom: 0; + } + } + + .chat-timestamp { + color: var(--dtp-chat-timestamp-color); + } + + .chat-sticker { + display: inline-block; + margin-top: 4px; + margin-right: 8px; + color: inherit; + + video { + width: auto; + height: 100px; + } + } + + .chat-user-menu { + + button.chat-menu-button { + padding: 0; + margin: 0; + background: transparent; + outline: none; + border: none; + line-height: 1; + } + } +} + #site-chat-container { overflow: auto; background-color: @content-container-color; @@ -145,88 +236,6 @@ background-color: @scrollbar-thumb-color; } - - .chat-message { - padding: @grid-small-gutter-vertical @grid-small-gutter-horizontal; - margin: (@grid-small-gutter-vertical / 2) @grid-small-gutter-horizontal; - - border: solid 1px @content-border-color; - border-radius: 8px; - - background: @content-background-color; - color: inherit; - font-size: var(--dtp-chat-font-size); - - &.system-message { - background: #e8e8e8; - color: #1a1a1a; - - &[data-message-type="info"] { - background: #068be4; - color: white; - } - &[data-message-type="warning"] { - background: #e4c306; - color: white; - } - &[data-message-type="error"] { - background: #ff00131a; - color: white; - } - } - - .chat-username { - font-weight: bold; - font-size: var(--dtp-chat-font-size); - line-height: 1; - color: var(--dtp-chat-username-color); - } - - img.chat-author-image { - width: auto; - height: 40px; - border-radius: 4px; - } - - .chat-content { - line-height: 1.2em; - font-size: var(--dtp-chat-font-size); - color: inherit; - overflow-wrap: break-word; - - p:last-child { - margin-bottom: 0; - } - } - - .chat-timestamp { - color: var(--dtp-chat-timestamp-color); - } - - .chat-sticker { - display: inline-block; - margin-top: 4px; - margin-right: 8px; - color: inherit; - - video { - width: auto; - height: 100px; - } - } - - .chat-user-menu { - - button.chat-menu-button { - padding: 0; - margin: 0; - background: transparent; - outline: none; - border: none; - line-height: 1; - } - } - } } .chat-message-menu { diff --git a/config/limiter.js b/config/limiter.js index a7c417d..9c587ca 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -293,15 +293,20 @@ module.exports = { * NewsroomController */ newsroom: { + getUnifiedFeed: { + total: 15, + expire: ONE_MINUTE, + message: 'You are fetching the unified feed too quickly', + }, getFeedView: { - total: 5, + total: 15, expire: ONE_MINUTE, - message: 'You are reading newsletters too quickly', + message: 'You are fetching news feeds too quickly', }, getIndex: { total: 60, expire: ONE_MINUTE, - message: 'You are fetching newsletters too quickly', + message: 'You are fetching the newsroom too quickly', }, }, diff --git a/docs/samples/controller.js b/docs/samples/controller.js index 3a55f4d..1a845e7 100644 --- a/docs/samples/controller.js +++ b/docs/samples/controller.js @@ -90,8 +90,15 @@ class HomeController extends SiteController { } module.exports = { - slug: 'home', - name: 'home', - isHome: true, + logId: 'home', + index: 'home', + className: 'HomeController', create: async (dtp) => { return new HomeController(dtp); }, + + /* + * This attribute must exist and be set to true on your Home controller to + * ensure that it is started last. This matters for ensuring that your root + * route is registered to ExpressJS last. + */ + isHome: true, }; \ No newline at end of file diff --git a/docs/samples/service.js b/docs/samples/service.js index e7dc312..e620383 100644 --- a/docs/samples/service.js +++ b/docs/samples/service.js @@ -62,7 +62,8 @@ class SampleService extends SiteService { } module.exports = { + logId: 'svc:sample', name: 'sample', - slug: 'sample', + className: 'SampleService', create: (dtp) => { return new SampleService(dtp); }, }; \ No newline at end of file diff --git a/docs/samples/worker.js b/docs/samples/worker.js index d3c725c..5240c32 100644 --- a/docs/samples/worker.js +++ b/docs/samples/worker.js @@ -33,7 +33,7 @@ class SampleWorker extends SiteWorker { try { module.rootPath = path.resolve(__dirname, '..', '..'); module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); - module.component = { name: 'theWorkerName', slug: 'the-worker-name' }; + module.component = { logId: 'wrk:sample', index: 'sampleWorker', className: 'SampleWorker' }; module.config = { environment: process.env.NODE_ENV, diff --git a/docs/services.md b/docs/services.md index 79d1417..69e71ec 100644 --- a/docs/services.md +++ b/docs/services.md @@ -33,8 +33,9 @@ class MyService extends SiteService { } module.exports = { - slug: 'my-service', - name: 'myService', + logId: 'my-service', + index: 'myService', + className: 'MyService', create: (dtp) => { return new MyService(dtp); }, }; ``` diff --git a/dtp-media-engine.js b/dtp-media-engine.js new file mode 100644 index 0000000..1c11398 --- /dev/null +++ b/dtp-media-engine.js @@ -0,0 +1,178 @@ +// dtp-media-engine.js +// Copyright (C) 2022 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +require('dotenv').config(); + +const path = require('path'); + +const mongoose = require('mongoose'); +const mediasoup = require('mediasoup'); + +const { SiteAsync, SiteCommon, SitePlatform, SiteLog } = require(path.join(__dirname, 'lib', 'site-lib')); + +module.rootPath = __dirname; +module.pkg = require(path.join(module.rootPath, 'package.json')); +module.config = { + component: { logId: 'dtp-media-engine', index: 'dtpMediaEngine', className: 'DtpMediaEngine' }, + root: module.rootPath, + site: require(path.join(module.rootPath, 'config', 'site')), + webRtcServer: [ + { + protocol: 'udp', + ip: process.env.MEDIASOUP_WEBRTC_BIND_ADDR || '127.0.0.1', + port: process.env.MEDIASOUP_WEBRTC_BIND_PORT || 20000, + } + ] +}; + +module.log = new SiteLog(module, module.config.component); + +class MediaEngineWorker extends SiteCommon { + + constructor ( ) { + super(module, { logId: 'dtp-media-worker', index: 'dtpMediaWorker', className: 'MediaEngineWorker' }); + this._id = mongoose.Types.ObjectId(); + } + + async start ( ) { + await super.start(); + + try { + this.worker = await mediasoup.createWorker({ + logLevel: 'warn', + dtlsCertificateFile: process.env.HTTPS_SSL_CRT, + dtlsPrivateKeyFile: process.env.HTTPS_SSL_KEY, + }); + } catch (error) { + throw new Error(`failed to start mediasoup worker process: ${error.message}`); + } + + try { + const BIND_PORT = 20000 + module.nextWorkerIdx++; + this.webRtcServer = await this.worker.createWebRtcServer({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + port: BIND_PORT, + }, + { + protocol: 'tcp', + ip: '127.0.0.1', + port: BIND_PORT, + }, + ], + }); + } catch (error) { + throw new Error(`failed to start mediasoup WebRTC Server: ${error.message}`); + } + } + + async stop ( ) { + if (this.webRtcServer && !this.webRtcServer.closed) { + this.log.info('closing mediasoup WebRTC server'); + this.webRtcServer.close(); + delete this.webRtcServer; + } + + if (this.worker && !this.worker.closed) { + this.log.info('closing mediasoup worker process'); + this.worker.close(); + delete this.worker; + } + + await super.stop(); + } +} + +module.onNewWorker = async (worker) => { + module.log.info('new worker created', { worker: worker.pid }); + worker.observer.on('close', ( ) => { + module.log.info('worker shutting down', { worker: worker.pid }); + }); + + worker.observer.on('newrouter', (router) => { + module.log.info('new router created', { worker: worker.pid, router: router.id }); + router.observer.on('close', ( ) => { + module.log.info('router shutting down', { worker: worker.pid, router: router.id }); + }); + }); +}; + +module.createWorker = async ( ) => { + const worker = new MediaEngineWorker(); + module.workers.push(worker); + await worker.start(); +}; + +module.shutdown = async ( ) => { + await SiteAsync.each(module.workers, async (worker) => { + try { + await worker.stop(); + } catch (error) { + module.log.error('failed to stop worker', { error }); + } + }); +}; + +/* + * SERVER PROCESS INIT + */ + +(async ( ) => { + + process.on('unhandledRejection', (error, p) => { + module.log.error('Unhandled rejection', { + error: error, + promise: p, + stack: error.stack + }); + }); + + process.on('warning', (error) => { + module.log.alert('warning', { error }); + }); + + process.once('SIGINT', async ( ) => { + module.log.info('SIGINT received'); + module.log.info('requesting shutdown...'); + await module.shutdown(); + const exitCode = await SitePlatform.shutdown(); + process.nextTick(( ) => { + process.exit(exitCode); + }); + }); + + process.once('SIGUSR2', async ( ) => { + await SitePlatform.shutdown(); + process.kill(process.pid, 'SIGUSR2'); + }); + + try { + await SitePlatform.start(module); + } catch (error) { + module.log.error(`failed to start DTP ${module.config.component.className} process`, { error }); + return; + } + + try { + module.log.info('registering mediasoup observer callbacks'); + mediasoup.observer.on('newworker', module.onNewWorker); + + module.log.info('creating mediasoup worker instance'); + + module.nextWorkerIdx = 0; + module.workers = [ ]; + + await module.createWorker(); + + module.log.info('DTP Media Engine online'); + } catch (error) { + module.log.error('failed to start DTP Media Engine', { error }); + process.exit(-1); + } + +})(); \ No newline at end of file diff --git a/dtp-sites-cli.js b/dtp-sites-cli.js index 116f423..2b1977d 100644 --- a/dtp-sites-cli.js +++ b/dtp-sites-cli.js @@ -4,8 +4,6 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Sites CLI', slug: 'sites-cli' }; - require('dotenv').config(); const path = require('path'); @@ -22,10 +20,10 @@ const { module.rootPath = __dirname; module.pkg = require(path.join(module.rootPath, 'package.json')); module.config = { - component: DTP_COMPONENT, root: module.rootPath, site: require(path.join(module.rootPath, 'config', 'site')), http: require(path.join(module.rootPath, 'config', 'http')), + component: { logId: 'sites-cli', index: 'sitesCli', className: 'SitesCli' }, }; module.log = new SiteLog(module, module.config.component); @@ -197,6 +195,17 @@ module.requestCoreConnect = async (host) => { module.log.info('connect tranaction', { txConnect }); }; +module.updatePageCache = async ( ) => { + const { page: pageService } = module.services; + try { + module.log.info('rebulding Page cache'); + await pageService.cacheMainMenuPages(); + } catch (error) { + module.log.error('failed to update page cache', { error }); + // fall through + } +}; + /* * SERVER INIT */ @@ -223,9 +232,9 @@ module.requestCoreConnect = async (host) => { ]); try { - await SitePlatform.startPlatform(module); + await SitePlatform.start(module); } catch (error) { - module.log.error(`failed to start DTP ${module.config.component.slug} platform`, { error }); + module.log.error(`failed to start DTP ${module.config.component.className} platform`, { error }); return; } @@ -270,7 +279,18 @@ module.requestCoreConnect = async (host) => { case 'welcome-email': await module.sendWelcomeEmail(target); break; + + /* + * Sites commands + */ + + case 'update-page-cache': + await module.updatePageCache(target); + break; + /* + * Error + */ default: throw new Error(`invalid action: ${module.app.options.action}`); } diff --git a/dtp-sites.js b/dtp-sites.js index 6f03d3e..36510fb 100644 --- a/dtp-sites.js +++ b/dtp-sites.js @@ -4,7 +4,11 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Sites', slug: 'sites' }; +const DTP_COMPONENT = { + logId: 'sites-app', + index: 'sitesApp', + className: 'SitesApp', +}; require('dotenv').config(); @@ -76,7 +80,7 @@ module.shutdown = async ( ) => { }); try { - await SitePlatform.startPlatform(module); + await SitePlatform.start(module); await SitePlatform.startWebServer(module); } catch (error) { module.log.error(`failed to start DTP ${module.config.component.name}`, { error }); diff --git a/lib/client/js/dtp-app.js b/lib/client/js/dtp-app.js index 6689f6b..6621d86 100644 --- a/lib/client/js/dtp-app.js +++ b/lib/client/js/dtp-app.js @@ -18,6 +18,8 @@ export default class DtpApp { this.log.debug('constructor', 'creating DisplayEngine instance'); this.displayEngine = new DtpDisplayEngine(); + + this.updateTimestamps(); } async connect (options) { @@ -203,4 +205,38 @@ export default class DtpApp { return isValid; } + + /** + * Finds every element with the `data-dtp-timestamp` attribute, and sets the + * text content of that element to the client-local representation of that + * timestamp using the formatting provided by the `data-dtp-timestamp-format` + * attribute. + */ + updateTimestamps ( ) { + const nodeList = document.querySelectorAll("[data-dtp-timestamp]"); + for (const ts of nodeList) { + const date = ts.getAttribute('data-dtp-timestamp'); + if (!date) { + continue; + } + + const format = ts.getAttribute('data-dtp-timestamp-format'); + switch (format) { + case 'date': + ts.textContent = moment(date).format('MMM DD, YYYY'); + break; + case 'datetime': + ts.textContent = moment(date).format('MMMM D, h:mm a'); + break; + case 'fuzzy': + ts.textContent = moment(date).fromNow(); + break; + + case 'timestamp': + default: + ts.textContent = moment(date).format('hh:mm:ss a'); + break; + } + } + } } \ No newline at end of file diff --git a/lib/client/js/dtp-display-engine.js b/lib/client/js/dtp-display-engine.js index 0005dcc..448ff00 100644 --- a/lib/client/js/dtp-display-engine.js +++ b/lib/client/js/dtp-display-engine.js @@ -4,17 +4,17 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Display Engine', slug: 'display-engine' }; - import UIkit from 'uikit'; import DtpLog from './dtp-log.js'; export default class DtpDisplayEngine { + static get COMPONENT ( ) { return { logId: 'display-engine', index: 'displayEngine', className: 'DisplayEngine' }; } + constructor (app) { this.app = app; this.processors = { }; - this.log = new DtpLog(DTP_COMPONENT); + this.log = new DtpLog(DtpDisplayEngine.COMPONENT); } /** diff --git a/lib/client/js/dtp-log.js b/lib/client/js/dtp-log.js index 1746cc6..17dc94d 100644 --- a/lib/client/js/dtp-log.js +++ b/lib/client/js/dtp-log.js @@ -113,13 +113,13 @@ export default class DtpWebLog { if (!this.enabled) { return; } if (data) { console[method]('%c%s%c: %s', - css.label, `${this.component.slug}.${event}`, + css.label, `${this.component.logId}.${event}`, css.message, msg, data, ); } else { console[method]('%c%s%c: %s', - css.label, `${this.component.slug}.${event}`, + css.label, `${this.component.logId}.${event}`, css.message, msg, ); } diff --git a/lib/client/js/dtp-socket.js b/lib/client/js/dtp-socket.js index be63d47..22c54f8 100644 --- a/lib/client/js/dtp-socket.js +++ b/lib/client/js/dtp-socket.js @@ -4,19 +4,19 @@ 'use strict'; -const DTP_COMPONENT = { name: 'Socket', slug: 'socket' }; - window.dtp = window.dtp || { }; import DtpWebLog from './dtp-log.js'; export default class DtpWebSocket { + static get COMPONENT ( ) { return { logId: 'socket', index: 'socket', className: 'DtpWebSocket' }; } + constructor ( ) { this.isConnected = false; this.isAuthenticated = false; this.joinedChannels = { }; - this.log = new DtpWebLog(DTP_COMPONENT); + this.log = new DtpWebLog(DtpWebSocket.COMPONENT); } async connect (options) { diff --git a/lib/site-common.js b/lib/site-common.js index 2b62eb3..0472bfe 100644 --- a/lib/site-common.js +++ b/lib/site-common.js @@ -19,7 +19,12 @@ class SiteCommon extends Events { super(); this.dtp = dtp; - this.component = component; + + this.component = { + logId: component.logId, + index: component.index, + className: component.className, + }; this.log = new SiteLog(dtp, component); this.appTemplateRoot = path.join(this.dtp.config.root, 'app', 'templates'); @@ -43,6 +48,14 @@ class SiteCommon extends Events { }, 1); } + getEventName (name) { + return `dtp.${this.component.logId}.${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-controller.js b/lib/site-controller.js index 2bf96a0..4440c60 100644 --- a/lib/site-controller.js +++ b/lib/site-controller.js @@ -40,12 +40,12 @@ class SiteController extends SiteCommon { createMulter (slug, options) { if (!!slug && (typeof slug === 'object')) { options = slug; - slug = this.component.slug; + slug = this.component.logId; } else { - slug = slug || this.component.slug; + slug = slug || this.component.logId; } options = Object.assign({ - dest: `/tmp/${this.dtp.config.site.domainKey}/${slug}/${this.component.slug}` + dest: `/tmp/${this.dtp.config.site.domainKey}/${slug}`, }, options || { }); return multer(options); diff --git a/lib/site-ioserver.js b/lib/site-ioserver.js index bfcbae1..7774b81 100644 --- a/lib/site-ioserver.js +++ b/lib/site-ioserver.js @@ -4,7 +4,6 @@ 'use strict'; -const DTP_COMPONENT = { name: 'I/O Server', slug: 'ioserver', prefix: 'srv' }; const path = require('path'); const Redis = require('ioredis'); @@ -14,15 +13,13 @@ const ConnectToken = mongoose.model('ConnectToken'); const marked = require('marked'); -const { SiteLog } = require(path.join(__dirname, 'site-log')); const { SiteCommon } = require(path.join(__dirname, 'site-common')); class SiteIoServer extends SiteCommon { constructor (dtp) { - super(dtp, { name: 'ioServer', slug: 'io-server' }); + super(dtp, { logId: 'io-server', index: 'ioServer', className: 'SiteIoServer' }); this.dtp = dtp; - this.log = new SiteLog(dtp, DTP_COMPONENT); } async start (httpServer) { diff --git a/lib/site-log.js b/lib/site-log.js index 89810f9..649e3ed 100644 --- a/lib/site-log.js +++ b/lib/site-log.js @@ -37,7 +37,10 @@ class SiteLog { } this.dtp = dtp; - if (!component || !component.slug || !component.name) { + if (!component || + !component.logId || + !component.index || + !component.className) { throw new Error('Must specify DTP component'); } this.component = component; @@ -83,7 +86,7 @@ class SiteLog { async writeLog (level, message, metadata) { const NOW = new Date(); const ctimestamp = color.black(moment(NOW).format('YYYY-MM-DD HH:mm:ss.SSS')); - const ccomponentSlug = color.cyan(this.component.slug); + const ccomponentSlug = color.cyan(this.component.logId); const cmessage = color.darkGray(message); let clevel = level.padEnd(5); @@ -121,8 +124,13 @@ class SiteLog { if (LogModel && (process.env.DTP_LOG_MONGODB === 'enabled')) { try { - const component = { name: this.component.name, slug: this.component.slug }; - await LogModel.create({ created: NOW, level, component, message, metadata }); + await LogModel.create({ + created: NOW, + component: this.component, + level, + message, + metadata, + }); } catch (error) { console.log( 'failed to write log entry to MongoDB', @@ -135,7 +143,7 @@ class SiteLog { if (LogStream && (process.env.DTP_LOG_FILE === 'enabled')) { const logEntry = { - t: NOW, c: this.component.slug, l: level, m: message, d: metadata, + t: NOW, c: this.component.logId, l: level, m: message, d: metadata, }; LogStream.write(`${JSON.stringify(logEntry)}\n`); } diff --git a/lib/site-platform.js b/lib/site-platform.js index 0361ad5..b455876 100644 --- a/lib/site-platform.js +++ b/lib/site-platform.js @@ -133,14 +133,13 @@ module.getRedisKeys = (pattern) => { }; module.loadServices = async (dtp) => { - dtp.services = module.services = { }; + dtp.services = { }; const scripts = glob.sync(path.join(dtp.config.root, 'app', 'services', '*.js')); const inits = [ ]; await SiteAsync.each(scripts, async (script) => { const service = await require(script); - module.services[service.name] = service.create(dtp); - module.services[service.name].__dtp_service_name = service.name; - inits.push(module.services[service.name]); + dtp.services[service.index] = service.create(dtp); + inits.push(dtp.services[service.index]); }); await SiteAsync.each(inits, async (service) => { await service.start(); @@ -156,8 +155,12 @@ module.loadControllers = async (dtp) => { await SiteAsync.each(scripts, async (script) => { const controller = await require(script); controller.instance = await controller.create(dtp); - module.log.info('controller loaded', { name: controller.name, slug: controller.slug }); - dtp.controllers[controller.name] = controller; + module.log.info('controller loaded', { + logId: controller.logId, + index: controller.index, + className: controller.className, + }); + dtp.controllers[controller.index] = controller; inits.push(controller); }); @@ -190,7 +193,7 @@ module.loadControllers = async (dtp) => { }); }; -module.exports.startPlatform = async (dtp) => { +module.exports.start = async (dtp) => { try { module.log = new SiteLog(module, dtp.config.component); @@ -342,9 +345,9 @@ module.exports.startWebServer = async (dtp) => { module.app.use(passport.initialize()); module.app.use(passport.session()); - module.services.oauth2.registerPassport(); - module.app.use(module.services.session.middleware()); - module.app.use(module.services.userNotification.middleware({ withNotifications: false })); + dtp.services.oauth2.registerPassport(); + module.app.use(dtp.services.session.middleware()); + module.app.use(dtp.services.userNotification.middleware({ withNotifications: false })); /* * Application logic middleware @@ -411,17 +414,16 @@ module.exports.startWebServer = async (dtp) => { await module.createHttpsServer(dtp, module.app); } - // prefer to attach Socket.io to the HTTPS server and fall back to HTTP - await module.createSocketServer(dtp, module.https || module.http); - if (module.http) { + await module.createSocketServer(dtp, module.http); await module.startHttpServer(dtp, module.http, dtp.config.http); } if (module.https) { + await module.createSocketServer(dtp, module.https); await module.startHttpServer(dtp, module.https, dtp.config.https); } - module.log.info(`${dtp.config.component.name} platform online`, { + module.log.info(`${dtp.config.component.className} platform online`, { http: dtp.config.http.port, https: dtp.config.https.port, }); diff --git a/lib/site-worker-process.js b/lib/site-worker-process.js index 5d5ac46..57ac5ff 100644 --- a/lib/site-worker-process.js +++ b/lib/site-worker-process.js @@ -15,7 +15,7 @@ const { SiteCommon } = require(path.join(__dirname, 'site-common')); * Your derived class must implement a static getter for COMPONENT as follows: * * ``` - * static get COMPONENT ( ) { return { name: '', slug: '' }; } + * static get COMPONENT ( ) { return { logId: '', name: '', className: '' }; } * ``` * * It must pass that object to this constructor (super) along with the worker diff --git a/lib/site-worker.js b/lib/site-worker.js index 3ad0cce..7acd36d 100644 --- a/lib/site-worker.js +++ b/lib/site-worker.js @@ -48,7 +48,7 @@ class SiteWorker extends SiteCommon { /* * Site Platform startup */ - await SitePlatform.startPlatform(this.dtp); + await SitePlatform.start(this.dtp); } catch (error) { this.log.error('failed to start worker', { component: this.dtp.config.component, @@ -70,8 +70,8 @@ class SiteWorker extends SiteCommon { const processor = new ProcessorClass(this); const { COMPONENT } = ProcessorClass; - this.log.info('loading worker processor', { component: COMPONENT.name }); - this.processors[COMPONENT.name] = processor; + this.log.info('loading worker processor', { component: COMPONENT.logId }); + this.processors[COMPONENT.index] = processor; return processor; } @@ -83,20 +83,20 @@ class SiteWorker extends SiteCommon { */ async startProcessors ( ) { const slugs = Object.keys(this.processors); - await SiteAsync.each(slugs, async (slug) => { + for (const slug of slugs) { const processor = this.processors[slug]; try { this.log.info('starting worker processor', { - component: processor.component.name, + component: processor.component.logId, }); await processor.start(); } catch (error) { this.log.error('failed to start processor', { - component: processor.component.name, + component: processor.component.logId, error, }); } - }, 1); + } } /** @@ -108,12 +108,12 @@ class SiteWorker extends SiteCommon { const processor = this.processors[slug]; try { this.log.info('stopping worker processor', { - component: processor.component.name, + component: processor.component.logId, }); await processor.stop(); } catch (error) { this.log.error('failed to stop processor', { - component: processor.component.name, + component: processor.component.logId, error, }); } diff --git a/package.json b/package.json index 137d845..fb9ce14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dtp-sites", - "version": "0.7.1", + "version": "0.7.13", "description": "Open source blogging engine for the Digital Telepresence Platform.", "main": "dtp-sites.js", "author": "DTP Technologies, LLC", @@ -28,7 +28,8 @@ "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", + "dtp-logan-api": "^0.4.3", "ein-validator": "^1.0.1", "email-domain-check": "^1.1.4", "email-validator": "^2.0.4", diff --git a/start-local b/start-local index bfd3a11..57c17dd 100755 --- a/start-local +++ b/start-local @@ -9,6 +9,7 @@ export MINIO_ROOT_USER MINIO_ROOT_PASSWORD MINIO_CI_CD forever start --killSignal=SIGINT app/workers/host-services.js forever start --killSignal=SIGINT app/workers/reeeper.js +forever start --killSignal=SIGINT app/workers/logan.js forever start --killSignal=SIGINT app/workers/newsletter.js forever start --killSignal=SIGINT app/workers/newsroom.js @@ -24,5 +25,6 @@ forever stop app/workers/media.js forever stop app/workers/newsroom.js forever stop app/workers/newsletter.js +forever stop app/workers/logan.js forever stop app/workers/reeeper.js forever stop app/workers/host-services.js \ No newline at end of file diff --git a/start-production b/start-production index 6472486..9ef77ff 100755 --- a/start-production +++ b/start-production @@ -3,6 +3,7 @@ sudo supervisord ctl start \ sites-host-services:* \ sites-reeeper:* \ + sites-logan:* \ sites-newsletter:* \ sites-newsroom:* \ sites-media:* \ diff --git a/stop-production b/stop-production index 857dd8d..9884db9 100755 --- a/stop-production +++ b/stop-production @@ -6,5 +6,6 @@ sudo supervisord ctl stop \ sites-media:* \ sites-newsroom:* \ sites-newsletter:* \ + sites-logan:* \ sites-reeeper:* \ - sites-host-services:* \ \ No newline at end of file + sites-host-services:* \ No newline at end of file diff --git a/supervisord/dtp-sites-chat.conf b/supervisord/dtp-sites-chat.conf index 748fec9..14f953c 100644 --- a/supervisord/dtp-sites-chat.conf +++ b/supervisord/dtp-sites-chat.conf @@ -1,16 +1,13 @@ -[group:sites-chat] -programs=dtp-sites-chat - -[program:dtp-sites-chat] +[program:sites-chat] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/chat.js -directory=/home/cybershell/live/dtp-sites +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/chat.js +directory=/home/dtp/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT -stderr_logfile=/var/log/dtp-sites/host-services.err.log -stdout_logfile=/var/log/dtp-sites/host-services.out.log -user=cybershell -environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-chat \ No newline at end of file +stderr_logfile=/var/log/dtp/dtp-sites-chat.err.log +stdout_logfile=/var/log/dtp/dtp-sites-chat.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',NODE_ENV=production,LOGNAME=dtp-sites-chat diff --git a/supervisord/dtp-sites-host-services.conf b/supervisord/dtp-sites-host-services.conf index 3dde470..50a9e71 100644 --- a/supervisord/dtp-sites-host-services.conf +++ b/supervisord/dtp-sites-host-services.conf @@ -1,17 +1,13 @@ -[group:sites-host-services] -programs=host-services - - -[program:host-services] +[program:sites-host-services] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/host-services.js -directory=/home/cybershell/live/dtp-sites +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/host-services.js +directory=/home/dtp/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT -stderr_logfile=/var/log/dtp-sites/host-services.err.log -stdout_logfile=/var/log/dtp-sites/host-services.out.log -user=cybershell -environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=host-services \ No newline at end of file +stderr_logfile=/var/log/dtp/dtp-sites-host-services.err.log +stdout_logfile=/var/log/dtp/dtp-sites-host-services.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',NODE_ENV=production,LOGNAME=dtp-sites-host-services diff --git a/supervisord/dtp-sites-logan.conf b/supervisord/dtp-sites-logan.conf new file mode 100644 index 0000000..e70675d --- /dev/null +++ b/supervisord/dtp-sites-logan.conf @@ -0,0 +1,13 @@ +[program:dtp-sites:logan] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/logan.js +directory=/home/dtp/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stdout_logfile=/var/log/dtp/dtp-sites-logan.out.log +stderr_logfile=/var/log/dtp/dtp-sites-logan.err.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',NODE_ENV=production,LOGNAME=dtp-sites-logan \ No newline at end of file diff --git a/supervisord/dtp-sites-media.conf b/supervisord/dtp-sites-media.conf index 38852f0..e35255a 100644 --- a/supervisord/dtp-sites-media.conf +++ b/supervisord/dtp-sites-media.conf @@ -1,17 +1,13 @@ - -[group:sites-media] -programs=dtp-sites-media - -[program:dtp-sites-media] +[program:sites-media] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/media.js -directory=/home/cybershell/live/dtp-sites +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/media.js +directory=/home/dtp/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT -stderr_logfile=/var/log/dtp-sites/host-services.err.log -stdout_logfile=/var/log/dtp-sites/host-services.out.log -user=cybershell -environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-media \ No newline at end of file +stderr_logfile=/var/log/dtp/dtp-sites-media.err.log +stdout_logfile=/var/log/dtp/dtp-sites-media.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',NODE_ENV=production,LOGNAME=dtp-sites-media diff --git a/supervisord/dtp-sites-newsletter.conf b/supervisord/dtp-sites-newsletter.conf index b04f625..ce548eb 100644 --- a/supervisord/dtp-sites-newsletter.conf +++ b/supervisord/dtp-sites-newsletter.conf @@ -1,16 +1,13 @@ -[group:sites-newsletter] -programs=dtp-sites-newsletter - -[program:dtp-sites-newsletter] +[program:sites-newsletter] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsletter.js -directory=/home/cybershell/live/dtp-sites +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsletter.js +directory=/home/dtp/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT -stderr_logfile=/var/log/dtp-sites/newsletter.err.log -stdout_logfile=/var/log/dtp-sites/newsletter.out.log -user=cybershell -environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-newsletter \ No newline at end of file +stderr_logfile=/var/log/dtp/dtp-sites-newsletter.err.log +stdout_logfile=/var/log/dtp/dtp-sites-newsletter.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',NODE_ENV=production,LOGNAME=dtp-sites-newsletter diff --git a/supervisord/dtp-sites-newsroom.conf b/supervisord/dtp-sites-newsroom.conf index 53159dc..954a8d0 100644 --- a/supervisord/dtp-sites-newsroom.conf +++ b/supervisord/dtp-sites-newsroom.conf @@ -1,16 +1,13 @@ -[group:sites-newsroom] -programs=dtp-sites-newsroom - -[program:dtp-sites-newsroom] +[program:sites-newsroom] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsroom.js -directory=/home/cybershell/live/dtp-sites +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsroom.js +directory=/home/dtp/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT -stderr_logfile=/var/log/dtp-sites/newsroom.err.log -stdout_logfile=/var/log/dtp-sites/newsroom.out.log -user=cybershell -environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-newsroom \ No newline at end of file +stderr_logfile=/var/log/dtp/dtp-sites-newsroom.err.log +stdout_logfile=/var/log/dtp/dtp-sites-newsroom.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',NODE_ENV=production,LOGNAME=dtp-sites-newsroom diff --git a/supervisord/dtp-sites-reeeper.conf b/supervisord/dtp-sites-reeeper.conf new file mode 100644 index 0000000..a793f82 --- /dev/null +++ b/supervisord/dtp-sites-reeeper.conf @@ -0,0 +1,13 @@ +[program:sites-reeeper] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/reeeper.js +directory=/home/dtp/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stderr_logfile=/var/log/dtp/dtp-sites-reeeper.err.log +stdout_logfile=/var/log/dtp/dtp-sites-reeeper.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',NODE_ENV=production,LOGNAME=dtp-sites-reeeper \ No newline at end of file diff --git a/supervisord/dtp-sites.conf b/supervisord/dtp-sites.conf index dc8e153..bd3df88 100644 --- a/supervisord/dtp-sites.conf +++ b/supervisord/dtp-sites.conf @@ -1,16 +1,13 @@ -[group:sites] -programs=dtp-sites - -[program:dtp-sites] -numprocs=1 +[program:sites] +numprocs=2 process_name=%(program_name)s_%(process_num)02d -command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 dtp-sites.js -directory=/home/cybershell/live/dtp-sites +command=/home/dtp/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 dtp-sites.js +directory=/home/dtp/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT -stderr_logfile=/var/log/dtp-sites/dtp-sites.err.log -stdout_logfile=/var/log/dtp-sites/dtp-sites.out.log -user=cybershell -environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=dtp-sites \ No newline at end of file +stderr_logfile=/var/log/dtp/dtp-sites-%(process_num)02d.err.log +stdout_logfile=/var/log/dtp/dtp-sites-%(process_num)02d.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',HTTP_BIND_PORT=34%(process_num)02d,HTTPS_BIND_PORT=35%(process_num)02d,NODE_ENV=production,LOGNAME=dtp-sites diff --git a/update-deps.js b/update-deps.js index a81ac2e..400ed3f 100644 --- a/update-deps.js +++ b/update-deps.js @@ -16,7 +16,7 @@ module.pkg = require(path.join(module.rootPath, 'package.json')); module.pinnedPackages = require(path.join(module.rootPath, 'config', 'pinned-packages')); module.config = { - component: { name: 'Webapp Dependency Updater', slug: 'webapp-update-deps' }, + component: { logId: 'webapp-update-deps', name: 'dtpUpdateDeps', className: 'DtpUpdateDeps' }, }; module.log = new SiteLog(module, module.config.component); diff --git a/yarn.lock b/yarn.lock index af44a50..92d79ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2100,6 +2100,20 @@ builtin-modules@^3.1.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== +bull@^4.10.4: + version "4.10.4" + resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.4.tgz#db39ee0c3bfbe3b76f1f35db800501de5bba4f84" + integrity sha512-o9m/7HjS/Or3vqRd59evBlWCXd9Lp+ALppKseoSKHaykK46SmRjAilX98PgmOz1yeVaurt8D5UtvEt4bUjM3eA== + dependencies: + cron-parser "^4.2.1" + debuglog "^1.0.0" + get-port "^5.1.1" + ioredis "^5.0.0" + lodash "^4.17.21" + msgpackr "^1.5.2" + semver "^7.3.2" + uuid "^8.3.0" + bull@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/bull/-/bull-4.7.0.tgz#89442d4676117edd9f9a1359bb0edfb489595e70" @@ -2981,6 +2995,11 @@ denque@^2.0.1: resolved "https://registry.yarnpkg.com/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a" integrity sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -3147,6 +3166,14 @@ drange@^1.0.2: dependencies: chalk "^4.1.1" +dtp-logan-api@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/dtp-logan-api/-/dtp-logan-api-0.4.3.tgz#4f5f748d78c0f2673b23a08f2476d815ccfb01ee" + integrity sha512-QNR3/l33SokZh9TVKqbITv1zPwM+G01Ef2EVo54QxRD541nsDLvsQ4wMGHLfgjzESG7h9ncuA7XKuqdO7Be3gg== + dependencies: + bull "^4.10.4" + ioredis "^5.3.2" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -4724,6 +4751,21 @@ ioredis@^4.28.5: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ioredis@^5.0.0, ioredis@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7" + integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ioredis@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.2.2.tgz#212467e04f6779b4e0e800cece7bb7d3d7b546d2"