Merge remote-tracking branch 'dtp/master'

master
Andrew Woodlee 10 months ago
commit 1400fe2172

@ -84,6 +84,8 @@ MINIO_PORT=9000
MINIO_USE_SSL=disabled MINIO_USE_SSL=disabled
MINIO_ACCESS_KEY=dtp-sites MINIO_ACCESS_KEY=dtp-sites
MINIO_SECRET_KEY= MINIO_SECRET_KEY=
MINIO_ADMIN_BUCKET=site-admin
MINIO_IMAGE_BUCKET=site-images MINIO_IMAGE_BUCKET=site-images
MINIO_VIDEO_BUCKET=site-videos MINIO_VIDEO_BUCKET=site-videos
MINIO_ATTACHMENT_BUCKET=site-attachments MINIO_ATTACHMENT_BUCKET=site-attachments
@ -112,4 +114,14 @@ DTP_LOG_DEBUG=enabled
DTP_LOG_INFO=enabled DTP_LOG_INFO=enabled
DTP_LOG_WARN=enabled DTP_LOG_WARN=enabled
DTP_LOG_HTTP_FORMAT=combined 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

@ -10,7 +10,7 @@
"undef": true, "undef": true,
"unused": true, "unused": true,
"futurehostile": true, "futurehostile": true,
"esversion": 9, "esversion": 11,
"mocha": true, "mocha": true,
"globals": { "globals": {
"markdown": true, "markdown": true,

@ -30,6 +30,27 @@
"console": "integratedTerminal", "console": "integratedTerminal",
"args": ["--action=reset-indexes", "all"] "args": ["--action=reset-indexes", "all"]
}, },
{
"type": "node",
"request": "launch",
"name": "worker:newsletter",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder:dtp-base}/app/workers/newsletter.js",
"console": "integratedTerminal",
},
{
"type": "node",
"request": "launch",
"name": "worker:newsroom",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder:dtp-base}/app/workers/newsroom.js",
"console": "integratedTerminal",
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",

@ -43,10 +43,12 @@ class AdminController extends SiteController {
); );
router.use('/announcement', await this.loadChild(path.join(__dirname, 'admin', 'announcement'))); 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('/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-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('/core-user', await this.loadChild(path.join(__dirname, 'admin', 'core-user')));
router.use('/host', await this.loadChild(path.join(__dirname, 'admin', 'host'))); 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('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log'))); router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));
router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter'))); 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 { const {
chat: chatService,
comment: commentService,
coreNode: coreNodeService, coreNode: coreNodeService,
dashboard: dashboardService, dashboard: dashboardService,
venue: venueService, venue: venueService,
logan: loganService, logan: loganService,
user: userService,
} = this.dtp.services; } = this.dtp.services;
try {
res.locals.stats = { res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`;
userSignupHourly: await dashboardService.getUserSignupsPerHour(), res.locals.stats = {
memberCount: await User.estimatedDocumentCount(), userSignupHourly: await dashboardService.getUserSignupsPerHour(),
constellation: await coreNodeService.getConstellationStats(), 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}`; try {
res.locals.channels = await venueService.getChannels();
loganService.sendRequestEvent(module.exports, req, { } catch (error) {
level: 'info', // fall through
event: 'getHomeView', res.locals.channels = [ ];
}); }
res.render('admin/index'); 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 = { module.exports = {
slug: 'admin', logId: 'ctl:admin',
name: 'admin', index: 'admin',
className: 'AdminController', className: 'AdminController',
create: async (dtp) => { return new AdminController(dtp); }, create: async (dtp) => { return new AdminController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController } = require('../../../lib/site-lib');
class AnnouncementAdminController extends SiteController { class AdminAnnouncementController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -158,8 +158,8 @@ class AnnouncementAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'announcement', logId: 'ctl:admin:announcement',
slug: 'announcement', index: 'adminAnnouncement',
className: 'AnnouncementAdminController', className: 'AdminAnnouncementController',
create: async (dtp) => { return new AnnouncementAdminController(dtp); }, create: async (dtp) => { return new AdminAnnouncementController(dtp); },
}; };

@ -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); },
};

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController } = require('../../../lib/site-lib');
class ContentReportAdminController extends SiteController { class AdminContentReportController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -87,8 +87,8 @@ class ContentReportAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminContentReport', logId: 'ctl:admin:content-report',
slug: 'admin-content-report', index: 'adminContentReport',
className: 'ContentReportAdminController', className: 'AdminContentReportController',
create: async (dtp) => { return new ContentReportAdminController(dtp); }, create: async (dtp) => { return new AdminContentReportController(dtp); },
}; };

@ -9,7 +9,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class CoreNodeAdminController extends SiteController { class AdminCoreNodeController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -140,8 +140,8 @@ class CoreNodeAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminCoreNode', logId: 'ctl:admin:core-node',
slug: 'admin-core-node', index: 'adminCoreNode',
className: 'CoreNodeAdminController', className: 'AdminCoreNodeController',
create: async (dtp) => { return new CoreNodeAdminController(dtp); }, create: async (dtp) => { return new AdminCoreNodeController(dtp); },
}; };

@ -9,7 +9,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class CoreUserAdminController extends SiteController { class AdminCoreUserController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -88,8 +88,8 @@ class CoreUserAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminCoreUser', logId: 'ctl:admin:core-user',
slug: 'admin-core-user', index: 'adminCoreUser',
className: 'CoreUserAdminController', className: 'AdminCoreUserController',
create: async (dtp) => { return new CoreUserAdminController(dtp); }, create: async (dtp) => { return new AdminCoreUserController(dtp); },
}; };

@ -12,7 +12,7 @@ const NetHostStats = mongoose.model('NetHostStats');
const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib'); const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib');
class HostAdminController extends SiteController { class AdminHostController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -116,8 +116,8 @@ class HostAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminHost', logId: 'ctl:admin:host',
slug: 'admin-host', index: 'adminHost',
className: 'HostAdminController', className: 'AdminHostController',
create: async (dtp) => { return new HostAdminController(dtp); }, create: async (dtp) => { return new AdminHostController(dtp); },
}; };

@ -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); },
};

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class JobQueueAdminController extends SiteController { class AdminJobQueueController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -119,8 +119,8 @@ class JobQueueAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminJobQueue', logId: 'ctl:admin:job-queue',
slug: 'admin-job-queue', index: 'adminJobQueue',
className: 'JobQueueAdminController', className: 'AdminJobQueueController',
create: async (dtp) => { return new JobQueueAdminController(dtp); }, create: async (dtp) => { return new AdminJobQueueController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController } = require('../../../lib/site-lib');
class LogAdminController extends SiteController { class AdminLogController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -32,12 +32,12 @@ class LogAdminController extends SiteController {
try { try {
res.locals.query = req.query; res.locals.query = req.query;
res.locals.components = await logService.getComponentSlugs(); res.locals.components = await logService.getComponentIds();
res.locals.pagination = this.getPaginationParameters(req, 25); res.locals.pagination = this.getPaginationParameters(req, 25);
const search = { }; const search = { };
if (req.query.component) { 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); res.locals.logs = await logService.getRecords(search, res.locals.pagination);
@ -51,8 +51,8 @@ class LogAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminLog', logId: 'ctl:admin:log',
slug: 'admin-log', index: 'adminLog',
className: 'LogAdminController', className: 'AdminLogController',
create: async (dtp) => { return new LogAdminController(dtp); }, create: async (dtp) => { return new AdminLogController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class NewsletterAdminController extends SiteController { class AdminNewsletterController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -167,8 +167,8 @@ class NewsletterAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminNewsletter', logId: 'ctl:admin:newsletter',
slug: 'admin-newsletter', index: 'adminNewsletter',
className: 'NewsletterAdminController', className: 'AdminNewsletterController',
create: async (dtp) => { return new NewsletterAdminController(dtp); }, create: async (dtp) => { return new AdminNewsletterController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class NewsroomAdminController extends SiteController { class AdminNewsroomController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -158,8 +158,8 @@ class NewsroomAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'newsroomAdmin', logId: 'ctl:admin:newsroom',
slug: 'newsroom-admin', index: 'adminNewsroomAdmin',
className: 'NewsroomAdminController', className: 'AdminNewsroomController',
create: async (dtp) => { return new NewsroomAdminController(dtp); }, create: async (dtp) => { return new AdminNewsroomController(dtp); },
}; };

@ -9,14 +9,14 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class OtpAdminController extends SiteController { class AdminOtpController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
} }
async start ( ) { 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(); const router = express.Router();
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
@ -49,8 +49,8 @@ class OtpAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminOtp', logId: 'ctl:admin:otp',
slug: 'admin-opt', index: 'adminOtp',
className: 'OtpAdminController', className: 'AdminOtpController',
create: async (dtp) => { return new OtpAdminController(dtp); }, create: async (dtp) => { return new AdminOtpController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class PageController extends SiteController { class AdminPageController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -131,8 +131,8 @@ class PageController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminPage', logId: 'ctl:admin:page',
slug: 'admin-page', index: 'adminPage',
className: 'PageController', className: 'AdminPageController',
create: async (dtp) => { return new PageController(dtp); }, create: async (dtp) => { return new AdminPageController(dtp); },
}; };

@ -9,7 +9,7 @@ const multer = require('multer');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class PostController extends SiteController { class AdminPostController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -140,8 +140,8 @@ class PostController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminPost', logId: 'ctl:admin:post',
slug: 'admin-post', index: 'adminPost',
className: 'PostController', className: 'AdminPostController',
create: async (dtp) => { return new PostController(dtp); }, create: async (dtp) => { return new AdminPostController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class ServiceNodeAdminController extends SiteController { class AdminServiceNodeController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -128,8 +128,8 @@ class ServiceNodeAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminServiceNode', logId: 'ctl:admin:service-node',
slug: 'admin-service-node', index: 'adminServiceNode',
className: 'ServiceNodeAdminController', className: 'AdminServiceNodeController',
create: async (dtp) => { return new ServiceNodeAdminController(dtp); }, create: async (dtp) => { return new AdminServiceNodeController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController } = require('../../../lib/site-lib');
class SettingsAdminController extends SiteController { class AdminSettingsController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -121,12 +121,11 @@ class SettingsAdminController extends SiteController {
}); });
} }
} }
} }
module.exports = { module.exports = {
name: 'adminSettings', logId: 'ctl:admin:settings',
slug: 'admin-settings', index: 'adminSettings',
className: 'SettingsAdminController', className: 'AdminSettingsController',
create: async (dtp) => { return new SettingsAdminController(dtp); }, create: async (dtp) => { return new AdminSettingsController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController } = require('../../../lib/site-lib');
class SiteLinkAdminController extends SiteController { class AdminSiteLinkController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -103,8 +103,8 @@ class SiteLinkAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminSiteLink', logId: 'ctl:admin:site-link',
slug: 'admin-site-link', index: 'adminSiteLink',
className: 'SiteLinkAdminController', className: 'AdminSiteLinkController',
create: async (dtp) => { return new SiteLinkAdminController(dtp); }, create: async (dtp) => { return new AdminSiteLinkController(dtp); },
}; };

@ -8,13 +8,21 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class UserAdminController extends SiteController { class AdminUserController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
} }
async start ( ) { 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(); const router = express.Router();
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
res.locals.currentView = 'admin'; res.locals.currentView = 'admin';
@ -23,11 +31,26 @@ class UserAdminController extends SiteController {
}); });
router.param('localUserId', this.populateLocalUserId.bind(this)); 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.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('/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)); router.get('/', this.getHomeView.bind(this));
return router; 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) { async postUpdateLocalUser (req, res, next) {
const { const {
logan: loganService, logan: loganService,
@ -53,6 +142,11 @@ class UserAdminController extends SiteController {
this.log.debug('local user update', { action: req.body.action }); this.log.debug('local user update', { action: req.body.action });
switch (req.body.action) { switch (req.body.action) {
case 'update': 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); await userService.updateLocalForAdmin(res.locals.userAccount, req.body);
loganService.sendRequestEvent(module.exports, req, { loganService.sendRequestEvent(module.exports, req, {
level: 'info', level: 'info',
@ -68,6 +162,9 @@ class UserAdminController extends SiteController {
break; break;
case 'ban': 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); await userService.ban(res.locals.userAccount);
loganService.sendRequestEvent(module.exports, req, { loganService.sendRequestEvent(module.exports, req, {
level: 'info', 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) { async getHomeView (req, res, next) {
const { user: userService } = this.dtp.services; const { user: userService } = this.dtp.services;
try { try {
@ -114,8 +338,8 @@ class UserAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminUser', logId: 'ctl:admin:user',
slug: 'admin-user', index: 'adminUser',
className: 'UserAdminController', className: 'AdminUserController',
create: async (dtp) => { return new UserAdminController(dtp); }, create: async (dtp) => { return new AdminUserController(dtp); },
}; };

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class VenueAdminController extends SiteController { class AdminVenueController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -136,8 +136,8 @@ class VenueAdminController extends SiteController {
} }
module.exports = { module.exports = {
name: 'adminVenue', logId: 'ctl:admin:venue',
slug: 'admin-venue', index: 'adminVenue',
className: 'VenueAdminController', className: 'AdminVenueController',
create: async (dtp) => { return new VenueAdminController(dtp); }, create: async (dtp) => { return new AdminVenueController(dtp); },
}; };

@ -76,8 +76,8 @@ class AnnouncementController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'announcement', logId: 'ctl:announcement',
name: 'announcement', index: 'announcement',
className: 'AnnouncementController', className: 'AnnouncementController',
create: async (dtp) => { return new AnnouncementController(dtp); }, create: async (dtp) => { return new AnnouncementController(dtp); },
}; };

@ -339,8 +339,8 @@ class AuthController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'auth', logId: 'ctl:auth',
name: 'auth', index: 'auth',
className: 'AuthController', className: 'AuthController',
create: async (dtp) => { return new AuthController(dtp); }, create: async (dtp) => { return new AuthController(dtp); },
}; };

@ -188,10 +188,8 @@ class AuthorController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'author', logId: 'ctl:author',
name: 'author', index: 'author',
create: async (dtp) => { className: 'AuthorController',
let controller = new AuthorController(dtp); create: async (dtp) => { return new AuthorController(dtp); },
return controller;
},
}; };

@ -366,16 +366,21 @@ class ChatController extends SiteController {
async getRoomEditor (req, res) { async getRoomEditor (req, res) {
const { logan: loganService } = this.dtp.services; 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, { loganService.sendRequestEvent(module.exports, req, {
level: 'info', level: 'info',
event: 'getRoomEditor', event: 'getRoomEditor',
data: { data: logData,
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
},
}); });
res.render('chat/room/editor'); res.render('chat/room/editor');
} }
@ -643,8 +648,8 @@ class ChatController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'chat', logId: 'ctl:chat',
name: 'chat', index: 'chat',
className: 'ChatController', className: 'ChatController',
create: async (dtp) => { return new ChatController(dtp); }, create: async (dtp) => { return new ChatController(dtp); },
}; };

@ -25,7 +25,7 @@ class CommentController extends SiteController {
dtp.app.use('/comment', router); dtp.app.use('/comment', router);
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
res.locals.currentView = module.exports.slug; res.locals.currentView = module.exports.logId;
return next(); return next();
}); });
@ -151,8 +151,8 @@ class CommentController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'comment', logId: 'ctl:comment',
name: 'comment', index: 'comment',
className: 'CommentController', className: 'CommentController',
create: async (dtp) => { return new CommentController(dtp); }, create: async (dtp) => { return new CommentController(dtp); },
}; };

@ -100,10 +100,8 @@ class ContentReportController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'content-report', logId: 'svc:content-report',
name: 'contentReport', index: 'contentReport',
create: async (dtp) => { className: 'ContentReportController',
let controller = new ContentReportController(dtp); create: async (dtp) => { return new ContentReportController(dtp); },
return controller;
},
}; };

@ -81,8 +81,8 @@ class EmailController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'email', logId: 'ctl:email',
name: 'email', index: 'email',
className: 'EmailController', className: 'EmailController',
create: async (dtp) => { return new EmailController(dtp); }, create: async (dtp) => { return new EmailController(dtp); },
}; };

@ -86,7 +86,8 @@ class SiteFeedController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'feed', logId: 'ctl:feed',
name: 'feed', index: 'feed',
className: 'SiteFeedController',
create: async (dtp) => { return new SiteFeedController(dtp); }, create: async (dtp) => { return new SiteFeedController(dtp); },
}; };

@ -41,7 +41,7 @@ class FormController extends SiteController {
sessionService.authCheckMiddleware({ requireLogin: true }), sessionService.authCheckMiddleware({ requireLogin: true }),
chatService.middleware({ maxOwnedRooms: 25, maxJoinedRooms: 50 }), chatService.middleware({ maxOwnedRooms: 25, maxJoinedRooms: 50 }),
async (req, res, next) => { async (req, res, next) => {
res.locals.currentView = module.exports.slug; res.locals.currentView = module.exports.logId;
return next(); return next();
}, },
); );
@ -64,8 +64,8 @@ class FormController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'form', logId: 'ctl:form',
name: 'form', index: 'form',
className: 'FormController', className: 'FormController',
create: async (dtp) => { return new FormController(dtp); }, create: async (dtp) => { return new FormController(dtp); },
}; };

@ -47,7 +47,7 @@ class HiveController extends SiteController {
res.locals.hiveView = 'home'; res.locals.hiveView = 'home';
res.status(200).json({ res.status(200).json({
pkg: { name: this.dtp.pkg.name, version: this.dtp.pkg.version }, 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, host: this.dtp.pkg.name,
description: this.dtp.pkg.description, description: this.dtp.pkg.description,
version: this.dtp.pkg.version, version: this.dtp.pkg.version,
@ -57,8 +57,8 @@ class HiveController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'hive', logId: 'ctl:hive',
name: 'hive', index: 'hive',
className: 'HiveController', className: 'HiveController',
create: async (dtp) => { return new HiveController(dtp); }, create: async (dtp) => { return new HiveController(dtp); },
}; };

@ -93,8 +93,8 @@ class HiveKaleidoscopeController extends SiteController {
} }
module.exports = { module.exports = {
name: 'hiveKaleidoscope', logId: 'ctl:hive:kaleidoscope',
slug: 'hive-kaleidoscope', index: 'hiveKaleidoscope',
className: 'HiveKaleidoscopeController', className: 'HiveKaleidoscopeController',
create: async (dtp) => { return new HiveKaleidoscopeController(dtp); }, create: async (dtp) => { return new HiveKaleidoscopeController(dtp); },
}; };

@ -55,7 +55,7 @@ class HiveUserController extends SiteController {
const { user: userService } = this.dtp.services; const { user: userService } = this.dtp.services;
try { try {
userId = mongoose.Types.ObjectId(userId); userId = mongoose.Types.ObjectId(userId);
res.locals.userProfile = await userService.getUserProfile(userId); res.locals.userProfile = await userService.getLocalUserProfile(userId);
if (!res.locals.userProfile) { if (!res.locals.userProfile) {
throw new SiteError(404, 'User profile not found'); 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.q = userService.filterUsername(req.query.q);
res.locals.pagination = this.getPaginationParameters(req, 20); 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) => { res.locals.userProfiles = res.locals.userProfiles.map((user) => {
const apiUser = userService.filterUserObject(user); const apiUser = userService.filterUserObject(user);
apiUser.picture.large = `/image/${user.picture.large}`; apiUser.picture.large = `/image/${user.picture.large}`;
@ -149,8 +149,8 @@ class HiveUserController extends SiteController {
} }
module.exports = { module.exports = {
name: 'hiveUser', logId: 'ctl:hive:user',
slug: 'hive-user', index: 'hiveUser',
className: 'HiveUserController', className: 'HiveUserController',
create: async (dtp) => { return new HiveUserController(dtp); }, create: async (dtp) => { return new HiveUserController(dtp); },
}; };

@ -96,9 +96,10 @@ class HomeController extends SiteController {
} }
module.exports = { module.exports = {
isHome: true, logId: 'ctl:home',
slug: 'home', index: 'home',
name: 'home',
className: 'HomeController', className: 'HomeController',
create: async (dtp) => { return new HomeController(dtp); }, create: async (dtp) => { return new HomeController(dtp); },
isHome: true,
}; };

@ -9,7 +9,7 @@ const fs = require('fs');
const express = require('express'); const express = require('express');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { SiteController/*, SiteError*/ } = require('../../lib/site-lib'); const { SiteController, SiteError } = require('../../lib/site-lib');
class ImageController extends SiteController { class ImageController extends SiteController {
@ -60,6 +60,9 @@ class ImageController extends SiteController {
try { try {
res.locals.imageId = mongoose.Types.ObjectId(imageId); res.locals.imageId = mongoose.Types.ObjectId(imageId);
res.locals.image = await this.dtp.services.image.getImageById(res.locals.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(); return next();
} catch (error) { } catch (error) {
this.log.error('failed to populate image', { error }); this.log.error('failed to populate image', { error });
@ -132,8 +135,8 @@ class ImageController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'image', logId: 'ctl:image',
name: 'image', index: 'image',
className: 'ImageController', className: 'ImageController',
create: async (dtp) => { return new ImageController(dtp); }, create: async (dtp) => { return new ImageController(dtp); },
}; };

@ -22,7 +22,7 @@ class ManifestController extends SiteController {
dtp.app.use('/manifest.json', router); dtp.app.use('/manifest.json', router);
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
res.locals.currentView = this.component.slug; res.locals.currentView = this.component.logId;
return next(); return next();
}); });
@ -64,8 +64,8 @@ class ManifestController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'manifest', logId: 'ctl:manifest',
name: 'manifest', index: 'manifest',
className: 'ManifestController', className: 'ManifestController',
create: async (dtp) => { return new ManifestController(dtp); }, create: async (dtp) => { return new ManifestController(dtp); },
}; };

@ -24,7 +24,7 @@ class NewsletterController extends SiteController {
dtp.app.use('/newsletter', router); dtp.app.use('/newsletter', router);
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
res.locals.currentView = module.exports.slug; res.locals.currentView = module.exports.logId;
return next(); return next();
}); });
@ -92,8 +92,8 @@ class NewsletterController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'newsletter', logId: 'ctl:newsletter',
name: 'newsletter', index: 'newsletter',
className: 'NewsletterController', className: 'NewsletterController',
create: async (dtp) => { return new NewsletterController(dtp); }, create: async (dtp) => { return new NewsletterController(dtp); },
}; };

@ -17,30 +17,39 @@ class NewsroomController extends SiteController {
async start ( ) { async start ( ) {
const { dtp } = this; const { dtp } = this;
const { limiter: limiterService } = dtp.services; const { limiter: limiterService } = dtp.services;
const limiterConfig = limiterService.config.newsroom;
const router = express.Router(); const router = express.Router();
dtp.app.use('/newsroom', router); dtp.app.use('/newsroom', router);
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
res.locals.currentView = module.exports.slug; res.locals.currentView = module.exports.logId;
return next(); return next();
}); });
router.param('feedId', this.populateFeedId.bind(this)); router.param('feedId', this.populateFeedId.bind(this));
router.get('/feed',
limiterService.createMiddleware(limiterConfig.getUnifiedFeed),
this.getUnifiedFeed.bind(this),
);
router.get('/:feedId', router.get('/:feedId',
limiterService.createMiddleware(limiterService.config.newsroom.getFeedView), limiterService.createMiddleware(limiterConfig.getFeedView),
this.getFeedView.bind(this), this.getFeedView.bind(this),
); );
router.get('/', router.get('/',
limiterService.createMiddleware(limiterService.config.newsletter.getIndex), limiterService.createMiddleware(limiterConfig.getIndex),
this.getHome.bind(this), this.getHome.bind(this),
); );
} }
async populateFeedId (req, res, next, feedId) { async populateFeedId (req, res, next, feedId) {
const { feed: feedService } = this.dtp.services; const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.feed = await feedService.getById(feedId); res.locals.feed = await feedService.getById(feedId);
if (!res.locals.feed) { if (!res.locals.feed) {
@ -48,39 +57,116 @@ class NewsroomController extends SiteController {
} }
return next(); return next();
} catch (error) { } 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); 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) { async getFeedView (req, res, next) {
const { feed: feedService } = this.dtp.services; const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 10); res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.newsroom = await feedService.getFeedEntries(res.locals.feed, res.locals.pagination); 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'); res.render('newsroom/feed-view');
} catch (error) { } 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); return next(error);
} }
} }
async getHome (req, res, next) { async getHome (req, res, next) {
const { feed: feedService } = this.dtp.services; const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 12); res.locals.pagination = this.getPaginationParameters(req, 12);
res.locals.newsroom = await feedService.getFeeds(res.locals.pagination, { withEntries: true }); res.locals.newsroom = await feedService.getFeeds(res.locals.pagination, { withEntries: true });
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHome',
});
res.render('newsroom/index'); res.render('newsroom/index');
} catch (error) { } 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); return next(error);
} }
} }
} }
module.exports = { module.exports = {
slug: 'newsroom', logId: 'ctl:newsroom',
name: 'newsroom', index: 'newsroom',
className: 'NewsroomController', className: 'NewsroomController',
create: (dtp) => { return new NewsroomController(dtp); }, create: (dtp) => { return new NewsroomController(dtp); },
}; };

@ -72,8 +72,8 @@ class NotificationController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'notification', logId: 'ctl:notification',
name: 'notification', index: 'notification',
className: 'NotificationController', className: 'NotificationController',
create: async (dtp) => { return new NotificationController(dtp); }, create: async (dtp) => { return new NotificationController(dtp); },
}; };

@ -35,7 +35,10 @@ class PageController extends SiteController {
} }
async populatePageSlug (req, res, next, pageSlug) { async populatePageSlug (req, res, next, pageSlug) {
const { page: pageService } = this.dtp.services; const {
logan: loganService,
page: pageService,
} = this.dtp.services;
try { try {
res.locals.page = await pageService.getBySlug(pageSlug); res.locals.page = await pageService.getBySlug(pageSlug);
if (!res.locals.page) { if (!res.locals.page) {
@ -43,28 +46,48 @@ class PageController extends SiteController {
} }
return next(); return next();
} catch (error) { } 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); return next(error);
} }
} }
async getView (req, res, next) { async getView (req, res, next) {
const { resource: resourceService } = this.dtp.services; const {
logan: loganService,
resource: resourceService,
} = this.dtp.services;
try { try {
await resourceService.recordView(req, 'Page', res.locals.page._id); await resourceService.recordView(req, 'Page', res.locals.page._id);
res.locals.pageSlug = res.locals.page.slug; res.locals.pageSlug = res.locals.page.slug;
res.locals.pageTitle = `${res.locals.page.title} on ${this.dtp.config.site.name}`; 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'); res.render('page/view');
} catch (error) { } catch (error) {
this.log.error('failed to service page view', { pageId: res.locals.page._id, 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); return next(error);
} }
} }
} }
module.exports = { module.exports = {
slug: 'page', logId: 'ctl:page',
name: 'page', index: 'page',
className: 'PageController', className: 'PageController',
create: async (dtp) => { return new PageController(dtp); }, create: async (dtp) => { return new PageController(dtp); },
}; };

@ -93,7 +93,6 @@ class PostController extends SiteController {
router.delete( router.delete(
'/:postId/profile-photo', '/:postId/profile-photo',
// limiterService.createMiddleware(limiterService.config.post.deletePostFeatureImage),
requireAuthorPrivileges, requireAuthorPrivileges,
this.deletePostFeatureImage.bind(this), this.deletePostFeatureImage.bind(this),
); );
@ -103,14 +102,15 @@ class PostController extends SiteController {
requireAuthorPrivileges, requireAuthorPrivileges,
this.deletePost.bind(this), this.deletePost.bind(this),
); );
router.get('/tag/:tagSlug', this.getTagSearchView.bind(this)); router.get('/tag/:tagSlug', this.getTagSearchView.bind(this));
} }
async populateUsername (req, res, next, username) { async populateUsername (req, res, next, username) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
res.locals.author = await userService.lookup(username); res.locals.author = await userService.lookup(username);
if (!res.locals.author) { if (!res.locals.author) {
@ -118,13 +118,21 @@ class PostController extends SiteController {
} }
return next(); return next();
} catch (error) { } 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); return next(error);
} }
} }
async populatePostSlug (req, res, next, postSlug) { async populatePostSlug (req, res, next, postSlug) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.post = await postService.getBySlug(postSlug); res.locals.post = await postService.getBySlug(postSlug);
if (!res.locals.post) { if (!res.locals.post) {
@ -132,13 +140,21 @@ class PostController extends SiteController {
} }
return next(); return next();
} catch (error) { } 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); return next(error);
} }
} }
async populatePostId (req, res, next, postId) { async populatePostId (req, res, next, postId) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.post = await postService.getById(postId); res.locals.post = await postService.getById(postId);
@ -147,13 +163,55 @@ class PostController extends SiteController {
return next(); return next();
} catch (error) { } 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); return next(error);
} }
} }
async postBlockCommentAuthor (req, res) { async postBlockCommentAuthor (req, res) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('add-recipient'); const displayList = this.createDisplayList('add-recipient');
await userService.blockUser(req.user._id, req.body.userId); await userService.blockUser(req.user._id, req.body.userId);
@ -163,9 +221,27 @@ class PostController extends SiteController {
'bottom-center', 'bottom-center',
4000, 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 }); res.status(200).json({ success: true, displayList });
} catch (error) { } 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({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -174,7 +250,10 @@ class PostController extends SiteController {
} }
async postComment (req, res) { async postComment (req, res) {
const { comment: commentService } = this.dtp.services; const {
comment: commentService,
logan: loganService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('add-recipient'); const displayList = this.createDisplayList('add-recipient');
@ -202,14 +281,38 @@ class PostController extends SiteController {
'bottom-center', 'bottom-center',
4000, 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 }); res.status(200).json({ success: true, displayList });
} catch (error) { } 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 }); res.status(error.statusCode || 500).json({ success: false, message: error.message });
} }
} }
async postUpdateImage (req, res) { async postUpdateImage (req, res) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('post-image'); const displayList = this.createDisplayList('post-image');
@ -221,9 +324,26 @@ class PostController extends SiteController {
'bottom-center', 'bottom-center',
2000, 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 }); res.status(200).json({ success: true, displayList });
} catch (error) { } 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({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -232,11 +352,22 @@ class PostController extends SiteController {
} }
async deletePostFeatureImage (req, res) { 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'}); res.status(500).json({ success: false, message: 'Removing the featured image is not yet implemented'});
} }
async postUpdatePost (req, res, next) { async postUpdatePost (req, res, next) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
if(!req.user.flags.isAdmin){ if(!req.user.flags.isAdmin){
if (!req.user._id.equals(res.locals.post.author._id) || 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); 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}`); res.redirect(`/post/${res.locals.post.slug}`);
} catch (error) { } 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); return next(error);
} }
} }
async postUpdatePostTags (req, res) { async postUpdatePostTags (req, res) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
if(!req.user.flags.isAdmin) if(!req.user.flags.isAdmin)
{ {
@ -269,9 +420,26 @@ class PostController extends SiteController {
'bottom-center', 'bottom-center',
2000, 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 }); res.status(200).json({ success: true, displayList });
} catch (error) { } 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({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -280,18 +448,41 @@ class PostController extends SiteController {
} }
async postCreatePost (req, res, next) { async postCreatePost (req, res, next) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.post = await postService.create(req.user, req.body); 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}`); res.redirect(`/post/${res.locals.post.slug}`);
} catch (error) { } 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); return next(error);
} }
} }
async getComments (req, res) { async getComments (req, res) {
const { comment: commentService } = this.dtp.services; const {
comment: commentService,
logan: loganService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('add-recipient'); const displayList = this.createDisplayList('add-recipient');
@ -322,9 +513,19 @@ class PostController extends SiteController {
const replyList = `ul#post-comment-list`; const replyList = `ul#post-comment-list`;
displayList.addElement(replyList, 'beforeEnd', html); displayList.addElement(replyList, 'beforeEnd', html);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getComments',
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } 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({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -333,7 +534,11 @@ class PostController extends SiteController {
} }
async getView (req, res, next) { async getView (req, res, next) {
const { comment: commentService, resource: resourceService } = this.dtp.services; const {
comment: commentService,
logan: loganService,
resource: resourceService,
} = this.dtp.services;
try { try {
if (res.locals.post.status !== 'published') { if (res.locals.post.status !== 'published') {
if (!req.user) { if (!req.user) {
@ -363,68 +568,156 @@ class PostController extends SiteController {
if (res.locals.post.image) { if (res.locals.post.image) {
res.locals.shareImage = `https://${this.dtp.config.site.domain}/image/${res.locals.post.image._id}`; 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'); res.render('post/view');
} catch (error) { } 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); return next(error);
} }
} }
async getEditor (req, res) { async getEditor (req, res, next) {
res.render('post/editor'); 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) { async getComposer (req, res, next) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.post = await postService.createPlaceholder(req.user); 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`); res.redirect(`/post/${res.locals.post._id}/edit`);
} catch (error) { } 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); return next(error);
} }
} }
async getIndex (req, res, next) { async getIndex (req, res, next) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination); res.locals.posts = await postService.getPosts(res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getIndex',
});
res.render('post/index'); res.render('post/index');
} catch (error) { } catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getIndex',
message: error.message,
data: { error },
});
return next(error); return next(error);
} }
} }
async getAuthorView (req, res, next) { async getAuthorView (req, res, next) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
const {posts, totalPostCount} = await postService.getForAuthor(res.locals.author, ['published'], res.locals.pagination); const {posts, totalPostCount} = await postService.getForAuthor(res.locals.author, ['published'], res.locals.pagination);
res.locals.posts = posts; res.locals.posts = posts;
res.locals.totalPostCount = totalPostCount; res.locals.totalPostCount = totalPostCount;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getAuthorView',
});
res.render('post/author/view'); res.render('post/author/view');
} catch (error) { } catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getAuthorView',
message: error.message,
data: { error },
});
return next(error); return next(error);
} }
} }
async getAllAuthorsView (req, res, next) { async getAllAuthorsView (req, res, next) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
const {authors , totalAuthorCount } = await userService.getAuthors(res.locals.pagination); const {authors , totalAuthorCount } = await userService.getAuthors(res.locals.pagination);
res.locals.authors = authors; res.locals.authors = authors;
res.locals.totalAuthorCount = totalAuthorCount; res.locals.totalAuthorCount = totalAuthorCount;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getAllAuthorsView',
});
res.render('post/author/all'); res.render('post/author/all');
} catch (error) { } catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getAllAuthorsView',
message: error.message,
data: { error },
});
return next(error); return next(error);
} }
} }
async deletePost (req, res) { async deletePost (req, res) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
// only give admins and the author permission to delete // only give admins and the author permission to delete
if (!req.user.flags.isAdmin) { if (!req.user.flags.isAdmin) {
@ -438,9 +731,29 @@ class PostController extends SiteController {
const displayList = this.createDisplayList('add-recipient'); const displayList = this.createDisplayList('add-recipient');
displayList.navigateTo('/'); 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 }); res.status(200).json({ success: true, displayList });
} catch (error) { } 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({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, 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) { async getTagSearchView (req, res) {
const { logan: loganService } = this.dtp.services;
try { try {
res.locals.pageTitle = `Tag ${res.locals.tag} on ${this.dtp.config.site.name}`; 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'); res.render('post/tag/view');
} catch (error) { } catch (error) {
this.log.error('failed to service post view', { postId: res.locals.post._id, 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) { async getTagIndex (req, res, next) {
const { post: postService } = this.dtp.services; const {
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination); res.locals.posts = await postService.getPosts(res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getTagIndex',
});
res.render('post/tag/index'); res.render('post/tag/index');
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
} }
} }
module.exports = { module.exports = {
slug: 'post', logId: 'ctl:post',
name: 'post', index: 'post',
className: 'PostController', className: 'PostController',
create: async (dtp) => { return new PostController(dtp); }, create: async (dtp) => { return new PostController(dtp); },
}; };

@ -119,13 +119,13 @@ class UserController extends SiteController {
); );
router.get( router.get(
'/:userId/otp-setup', '/:localUserId/otp-setup',
limiterService.createMiddleware(limiterService.config.user.getOtpSetup), limiterService.createMiddleware(limiterService.config.user.getOtpSetup),
otpSetup, otpSetup,
this.getOtpSetup.bind(this), this.getOtpSetup.bind(this),
); );
router.get( router.get(
'/:userId/otp-disable', '/:localUserId/otp-disable',
limiterService.createMiddleware(limiterService.config.user.getOtpDisable), limiterService.createMiddleware(limiterService.config.user.getOtpDisable),
authRequired, authRequired,
this.getOtpDisable.bind(this), this.getOtpDisable.bind(this),
@ -148,7 +148,7 @@ class UserController extends SiteController {
); );
router.delete( router.delete(
'/:userId/profile-photo', '/:localUserId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto), limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto),
authRequired, authRequired,
checkProfileOwner, checkProfileOwner,
@ -334,6 +334,12 @@ class UserController extends SiteController {
}); });
} catch (error) { } catch (error) {
this.log.error('failed to create new user', { 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); return next(error);
} }
} }
@ -639,8 +645,8 @@ class UserController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'user', logId: 'ctl:user',
name: 'user', index: 'user',
className: 'UserController', className: 'UserController',
create: async (dtp) => { return new UserController(dtp); }, create: async (dtp) => { return new UserController(dtp); },
}; };

@ -40,7 +40,10 @@ class VenueController extends SiteController {
} }
async populateChannelSlug (req, res, next, channelSlug) { async populateChannelSlug (req, res, next, channelSlug) {
const { venue: venueService } = this.dtp.services; const {
logan: loganService,
venue: venueService,
} = this.dtp.services;
try { try {
res.locals.channel = await venueService.getChannelBySlug(channelSlug, { withCredentials: true }); res.locals.channel = await venueService.getChannelBySlug(channelSlug, { withCredentials: true });
if (!res.locals.channel) { if (!res.locals.channel) {
@ -53,30 +56,59 @@ class VenueController extends SiteController {
return next(); return next();
} catch (error) { } catch (error) {
this.log.error('failed to populate Venue channel by slug', { channelSlug, 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); return next(error);
} }
} }
async getVenueEmbed (req, res) { async getVenueEmbed (req, res, next) {
res.render('venue/embed'); 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) { async getHome (req, res, next) {
const { venue: venueService} = this.dtp.services; const {
logan: loganService,
venue: venueService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 10); res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.channels = await venueService.getChannels(res.locals.pagination); res.locals.channels = await venueService.getChannels(res.locals.pagination);
res.render('venue/index'); res.render('venue/index');
} catch (error) { } 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); return next(error);
} }
} }
} }
module.exports = { module.exports = {
slug: 'venue', logId: 'ctl:venue',
name: 'venue', index: 'venue',
className: 'VenueController', className: 'VenueController',
create: async (dtp) => { return new VenueController(dtp); }, create: async (dtp) => { return new VenueController(dtp); },
}; };

@ -9,7 +9,7 @@ const path = require('path');
const express = require('express'); const express = require('express');
const captcha = require('svg-captcha'); const captcha = require('svg-captcha');
const { SiteController/*, SiteError */ } = require('../../lib/site-lib'); const { SiteController } = require('../../lib/site-lib');
class WelcomeController extends SiteController { class WelcomeController extends SiteController {
@ -120,8 +120,8 @@ class WelcomeController extends SiteController {
} }
module.exports = { module.exports = {
slug: 'welcome', logId: 'ctl:welcome',
name: 'welcome', index: 'welcome',
className: 'WelcomeController', className: 'WelcomeController',
create: async (dtp) => { return new WelcomeController(dtp); }, create: async (dtp) => { return new WelcomeController(dtp); },
}; };

@ -21,8 +21,9 @@ const LOG_LEVEL_LIST = [
const LogSchema = new Schema({ const LogSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1, expires: '7d' }, created: { type: Date, default: Date.now, required: true, index: -1, expires: '7d' },
component: { component: {
name: { type: String, required: true }, logId: { type: String, required: true, index: 1 },
slug: { 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 }, level: { type: String, enum: LOG_LEVEL_LIST, required: true, index: true },
message: { type: String }, message: { type: String },

@ -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);
};

@ -116,8 +116,8 @@ class AnnouncementService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'announcement', logId: 'svc:announcement',
name: 'announcement', index: 'announcement',
className: 'AnnouncementService', className: 'AnnouncementService',
create: (dtp) => { return new AnnouncementService(dtp); }, create: (dtp) => { return new AnnouncementService(dtp); },
}; };

@ -126,6 +126,23 @@ class AttachmentService extends SiteService {
return attachments; 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 * @param {mongoose.Types.ObjectId} attachmentId The ID of the attachment
@ -166,6 +183,20 @@ class AttachmentService extends SiteService {
return this.attachmentTemplate({ attachment, attachmentOptions }); 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 * Creates a Bull Queue job to delete an Attachment including it's processed
* and original media files. * and original media files.
@ -180,8 +211,8 @@ class AttachmentService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'attachment', logId: 'svc:attachment',
name: 'attachment', index: 'attachment',
className: 'AttachmentService', className: 'AttachmentService',
create: (dtp) => { return new AttachmentService(dtp); }, create: (dtp) => { return new AttachmentService(dtp); },
}; };

@ -57,8 +57,8 @@ class CacheService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'cache', logId: 'svc:cache',
name: 'cache', index: 'cache',
className: 'CacheService', className: 'CacheService',
create: (dtp) => { return new CacheService(dtp); }, create: (dtp) => { return new CacheService(dtp); },
}; };

@ -785,6 +785,16 @@ class ChatService extends SiteService {
return reaction.toObject(); 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) { async removeForUser (user) {
const { logan: loganService } = this.dtp.services; const { logan: loganService } = this.dtp.services;
@ -852,8 +862,8 @@ class ChatService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'chat', logId: 'svc:chat',
name: 'chat', index: 'chat',
className: 'ChatService', className: 'ChatService',
create: (dtp) => { return new ChatService(dtp); }, create: (dtp) => { return new ChatService(dtp); },
}; };

@ -251,6 +251,21 @@ class CommentService extends SiteService {
return comments; 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) { async getForAuthor (author, pagination) {
const comments = await Comment const comments = await Comment
.find({ // index: 'comment_author_by_type' .find({ // index: 'comment_author_by_type'
@ -335,8 +350,8 @@ class CommentService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'comment', logId: 'svc:comment',
name: 'comment', index: 'comment',
className: 'CommentService', className: 'CommentService',
create: (dtp) => { return new CommentService(dtp); }, create: (dtp) => { return new CommentService(dtp); },
}; };

@ -127,8 +127,8 @@ class ContentReportService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'content-report', logId: 'svc:content-report',
name: 'contentReport', index: 'contentReport',
className: 'ContentReportService', className: 'ContentReportService',
create: (dtp) => { return new ContentReportService(dtp); }, create: (dtp) => { return new ContentReportService(dtp); },
}; };

@ -117,8 +117,8 @@ class ContentVoteService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'content-vote', logId: 'svc:content-vote',
name: 'contentVote', index: 'contentVote',
className: 'ContentVoteService', className: 'ContentVoteService',
create: (dtp) => { return new ContentVoteService(dtp); }, create: (dtp) => { return new ContentVoteService(dtp); },
}; };

@ -716,8 +716,8 @@ class CoreNodeService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'core-node', logId: 'svc:core-node',
name: 'coreNode', index: 'coreNode',
className: 'CoreNodeService', className: 'CoreNodeService',
create: (dtp) => { return new CoreNodeService(dtp); }, create: (dtp) => { return new CoreNodeService(dtp); },
}; };

@ -58,8 +58,8 @@ class CryptoService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'crypto', logId: 'svc:crypto',
name: 'crypto', index: 'crypto',
className: 'CryptoService', className: 'CryptoService',
create: (dtp) => { return new CryptoService(dtp); }, create: (dtp) => { return new CryptoService(dtp); },
}; };

@ -76,8 +76,8 @@ class CsrfTokenService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'csrf-token', logId: 'svc:csrf-token',
name: 'csrfToken', index: 'csrfToken',
className: 'CsrfTokenService', className: 'CsrfTokenService',
create: (dtp) => { return new CsrfTokenService(dtp); }, create: (dtp) => { return new CsrfTokenService(dtp); },
}; };

@ -270,8 +270,8 @@ class DashboardService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'dashboard', logId: 'svc:dashboard',
name: 'dashboard', index: 'dashboard',
className: 'DashboardService', className: 'DashboardService',
create: (dtp) => { return new DashboardService(dtp); }, create: (dtp) => { return new DashboardService(dtp); },
}; };

@ -132,10 +132,6 @@ class DisplayEngineService extends SiteService {
this.templates = { }; this.templates = { };
} }
async start ( ) { }
async stop ( ) { }
loadTemplate (name, pugScript) { loadTemplate (name, pugScript) {
const scriptFile = path.join(this.dtp.config.root, 'app', 'views', pugScript); const scriptFile = path.join(this.dtp.config.root, 'app', 'views', pugScript);
this.templates[name] = pug.compileFile(scriptFile); this.templates[name] = pug.compileFile(scriptFile);
@ -154,8 +150,8 @@ class DisplayEngineService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'display-engine', logId: 'svc:display-engine',
name: 'displayEngine', index: 'displayEngine',
className: 'DisplayEngineService', className: 'DisplayEngineService',
create: (dtp) => { return new DisplayEngineService(dtp); }, create: (dtp) => { return new DisplayEngineService(dtp); },
}; };

@ -171,8 +171,8 @@ class EmailService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'email', logId: 'svc:email',
name: 'email', index: 'email',
className: 'EmailService', className: 'EmailService',
create: (dtp) => { return new EmailService(dtp); }, create: (dtp) => { return new EmailService(dtp); },
}; };

@ -237,8 +237,8 @@ class FeedService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'feed', logId: 'svc:feed',
name: 'feed', index: 'feed',
className: 'FeedService', className: 'FeedService',
create: (dtp) => { return new FeedService(dtp); }, create: (dtp) => { return new FeedService(dtp); },
}; };

@ -60,8 +60,8 @@ class GabTVService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'gab-tv', logId: 'svc:gab-tv',
name: 'gabTV', index: 'gabTV',
className: 'GabTVService', className: 'GabTVService',
create: (dtp) => { return new GabTVService(dtp); }, create: (dtp) => { return new GabTVService(dtp); },
}; };

@ -276,8 +276,8 @@ class HiveService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'hive', logId: 'svc:hive',
name: 'hive', index: 'hive',
className: 'HiveService', className: 'HiveService',
create: (dtp) => { return new HiveService(dtp); }, create: (dtp) => { return new HiveService(dtp); },
}; };

@ -97,8 +97,8 @@ class HostCacheService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'host-cache', logId: 'svc:host-cache',
name: 'hostCache', index: 'hostCache',
className: 'HostCacheService', className: 'HostCacheService',
create: (dtp) => { return new HostCacheService(dtp); }, create: (dtp) => { return new HostCacheService(dtp); },
}; };

@ -90,16 +90,37 @@ class ImageService extends SiteService {
return image; return image;
} }
async getRecentImagesForOwner(owner) { async getRecentImagesForOwner(owner, limit = 10) {
const images = await SiteImage const images = await SiteImage
.find({ owner: owner._id }) .find({ owner: owner._id })
.sort({ created: -1 }) .sort({ created: -1 })
.limit(10) .limit(limit)
.populate(this.populateImage) .populate(this.populateImage)
.lean(); .lean();
return images; 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) { async deleteImage(image) {
const { minio: minioService } = this.dtp.services; const { minio: minioService } = this.dtp.services;
@ -350,8 +371,8 @@ class ImageService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'image', logId: 'svc:image',
name: 'image', index: 'image',
className: 'ImageService', className: 'ImageService',
create: (dtp) => { return new ImageService(dtp); }, create: (dtp) => { return new ImageService(dtp); },
}; };

@ -62,8 +62,8 @@ class JobQueueService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'job-queue', logId: 'svc:job-queue',
name: 'jobQueue', index: 'jobQueue',
className: 'JobQueueService', className: 'JobQueueService',
create: (dtp) => { return new JobQueueService(dtp); }, create: (dtp) => { return new JobQueueService(dtp); },
}; };

@ -69,8 +69,8 @@ class LimiterService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'limiter', logId: 'svc:limiter',
name: 'limiter', index: 'limiter',
className: 'LimiterService', className: 'LimiterService',
create: (dtp) => { return new LimiterService(dtp); }, create: (dtp) => { return new LimiterService(dtp); },
}; };

@ -26,8 +26,8 @@ class SystemLogService extends SiteService {
return logs; return logs;
} }
async getComponentSlugs ( ) { async getComponentIds ( ) {
return await Log.distinct('component.slug'); return await Log.distinct('component.logId');
} }
async getTotalCount ( ) { async getTotalCount ( ) {
@ -38,8 +38,8 @@ class SystemLogService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'log', logId: 'svc:log',
name: 'log', index: 'log',
className: 'SystemLogService', className: 'SystemLogService',
create: (dtp) => { return new SystemLogService(dtp); }, create: (dtp) => { return new SystemLogService(dtp); },
}; };

@ -4,9 +4,7 @@
'use strict'; 'use strict';
const os = require('os'); const { SiteService } = require('../../lib/site-lib');
const { SiteService, SiteError } = require('../../lib/site-lib');
class LoganService extends SiteService { class LoganService extends SiteService {
@ -16,64 +14,62 @@ class LoganService extends SiteService {
async start ( ) { async start ( ) {
await super.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(); const { LoganClient } = await import('dtp-logan-api');
event['component.slug'] = component.slug;
event['component.name'] = component.className || component.name;
this.log[event.level]('sending Logan event', { event }); this.log.info('creating Logan client');
this.logan = new LoganClient();
const payload = JSON.stringify(event); this.log.info('initializing Logan client');
const response = await fetch(loganUrl, { await this.logan.initialize({
method: 'POST', log: this.log,
headers: { api: {
'Content-Type': 'application/json', enabled: process.env.DTP_LOGAN === 'enabled',
'Content-Length': payload.length, key: process.env.DTP_LOGAN_API_KEY,
'X-LogAn-Auth': 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(); async sendRequestEvent (component, req, event) {
if (!json.success) { return this.logan.sendRequestEvent(component, req, event);
throw new SiteError(500, json.message); }
}
return json; async sendEvent (component, event) {
} catch (error) { return this.logan.sendEvent(component, event);
this.log.error('failed to send LOGAN event', { event, error });
// fall through
}
} }
} }
module.exports = { module.exports = {
slug: 'logan', logId: 'svc:logan',
name: 'logan', index: 'logan',
className: 'LoganService', className: 'LoganService',
create: (dtp) => { return new LoganService(dtp); }, create: (dtp) => { return new LoganService(dtp); },
}; };

@ -32,8 +32,8 @@ class MarkdownService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'markdown', logId: 'svc:markdown',
name: 'markdown', index: 'markdown',
className: 'MarkdownService', className: 'MarkdownService',
create: (dtp) => { return new MarkdownService(dtp); }, create: (dtp) => { return new MarkdownService(dtp); },
}; };

@ -94,8 +94,8 @@ class MediaService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'media', logId: 'svc:media',
name: 'media', index: 'media',
className: 'MediaService', className: 'MediaService',
create: (dtp) => { return new MediaService(dtp); }, create: (dtp) => { return new MediaService(dtp); },
}; };

@ -24,7 +24,6 @@ class MinioService extends SiteService {
accessKey: process.env.MINIO_ACCESS_KEY, accessKey: process.env.MINIO_ACCESS_KEY,
secretKey: process.env.MINIO_SECRET_KEY, secretKey: process.env.MINIO_SECRET_KEY,
}; };
this.log.debug('MinIO config', { minioConfig });
this.minio = new Minio.Client(minioConfig); this.minio = new Minio.Client(minioConfig);
} }
@ -98,8 +97,8 @@ class MinioService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'minio', logId: 'svc:minio',
name: 'minio', index: 'minio',
className: 'MinioService', className: 'MinioService',
create: (dtp) => { return new MinioService(dtp); }, create: (dtp) => { return new MinioService(dtp); },
}; };

@ -117,8 +117,8 @@ class NewsletterService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'newsletter', logId: 'svc:newsletter',
name: 'newsletter', index: 'newsletter',
className: 'NewsletterService', className: 'NewsletterService',
create: (dtp) => { return new NewsletterService(dtp); }, create: (dtp) => { return new NewsletterService(dtp); },
}; };

@ -470,8 +470,8 @@ class OAuth2Service extends SiteService {
} }
module.exports = { module.exports = {
slug: 'oauth2', logId: 'svc:oauth2',
name: 'oauth2', index: 'oauth2',
className: 'OAuth2Service', className: 'OAuth2Service',
create: (dtp) => { return new OAuth2Service(dtp); }, create: (dtp) => { return new OAuth2Service(dtp); },
}; };

@ -238,8 +238,8 @@ class OtpAuthService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'otp-auth', logId: 'svc:otp-auth',
name: 'otpAuth', index: 'otpAuth',
className: 'OtpAuthService', className: 'OtpAuthService',
create: (dtp) => { return new OtpAuthService(dtp); }, create: (dtp) => { return new OtpAuthService(dtp); },
}; };

@ -7,7 +7,7 @@
const striptags = require('striptags'); const striptags = require('striptags');
const slug = require('slug'); const slug = require('slug');
const { SiteService, SiteError, SiteAsync } = require('../../lib/site-lib'); const { SiteService, SiteError } = require('../../lib/site-lib');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const ObjectId = mongoose.Types.ObjectId; const ObjectId = mongoose.Types.ObjectId;
@ -180,7 +180,6 @@ class PageService extends SiteService {
return this.getById(pageId); return this.getById(pageId);
} }
async isParentPage (page) { async isParentPage (page) {
if (page) { if (page) {
page = [page]; page = [page];
@ -204,9 +203,37 @@ class PageService extends SiteService {
return pages; return pages;
} }
async deletePage (page) { async deletePage (page, options) {
this.log.info('deleting page', { pageId: page._id }); options = Object.assign({ updateCache: true }, options);
this.log.info('deleting page', { pageId: page._id, options });
await Page.deleteOne({ _id: page._id }); 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) { createPageSlug (pageId, pageTitle) {
@ -217,90 +244,92 @@ class PageService extends SiteService {
return `${pageSlug}-${pageId}`; return `${pageSlug}-${pageId}`;
} }
async cacheMainMenuPages () { async cacheMainMenuPages ( ) {
try {
const pages = await Page
.find({ status: 'published' })
.select('slug menu')
.populate({path: 'menu.parent'})
.lean();
let mainMenu = []; let mainMenu = [];
await SiteAsync.each(pages, async (page) => {
if (page.menu.parent) { try {
let parent = page.menu.parent; await Page
if (parent.status === 'published') { .find({ status: 'published' })
let parentPage = mainMenu.find(item => item.slug === parent.slug); .select('slug menu')
if (parentPage) { .populate({path: 'menu.parent'})
let childPage = { .lean()
url: `/page/${page.slug}`, .cursor()
slug: page.slug, .eachAsync(async (page) => {
icon: page.menu.icon, if (page.menu.parent) {
label: page.menu.label, let parent = page.menu.parent;
order: page.menu.order, if (parent.status === 'published') {
}; let parentPage = mainMenu.find(item => item.slug === parent.slug);
parentPage.children.splice(childPage.order, 0, childPage); if (parentPage) {
} let childPage = {
else { url: `/page/${page.slug}`,
let parentPage = { slug: page.slug,
url: `/page/${parent.slug}`, icon: page.menu.icon,
slug: parent.slug, label: page.menu.label,
icon: parent.menu.icon, order: page.menu.order,
label: parent.menu.label, };
order: parent.menu.order, parentPage.children.splice(childPage.order, 0, childPage);
children: [], }
}; else {
let childPage = { let parentPage = {
url: `/page/${page.slug}`, url: `/page/${parent.slug}`,
slug: page.slug, slug: parent.slug,
icon: page.menu.icon, icon: parent.menu.icon,
label: page.menu.label, label: parent.menu.label,
order: page.menu.order, order: parent.menu.order,
}; children: [],
parentPage.children.splice(childPage.order, 0, childPage); };
mainMenu.splice(parentPage.order, 0, 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);
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, * Sort the menu data
icon: page.menu.icon, */
label: page.menu.label, mainMenu.sort((a, b) => a.order - b.order);
order: page.menu.order, for (const menu of mainMenu) {
children: [], if (menu.children) {
}; menu.children.sort((a, b) => a.order - b.order);
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);
} }
} }
});
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) { } catch (error) {
this.dtp.log.error('failed to build page menu', { error }); this.dtp.log.error('failed to build page menu', { error });
} }
@ -313,8 +342,8 @@ class PageService extends SiteService {
module.exports = { module.exports = {
slug: 'page', logId: 'svc:page',
name: 'page', index: 'page',
className: 'PageService', className: 'PageService',
create: (dtp) => { return new PageService(dtp); }, create: (dtp) => { return new PageService(dtp); },
}; };

@ -55,8 +55,8 @@ class PhoneService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'phone', logId: 'svc:phone',
name: 'phone', index: 'phone',
className: 'PhoneService', className: 'PhoneService',
create: (dtp) => { return new PhoneService(dtp); }, create: (dtp) => { return new PhoneService(dtp); },
}; };

@ -400,6 +400,23 @@ class PostService extends SiteService {
await Post.deleteOne({ _id: post._id }); 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) { createPostSlug (postId, postTitle) {
if ((typeof postTitle !== 'string') || (postTitle.length < 1)) { if ((typeof postTitle !== 'string') || (postTitle.length < 1)) {
throw new Error('Invalid input for making a post slug'); throw new Error('Invalid input for making a post slug');
@ -410,8 +427,8 @@ class PostService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'post', logId: 'svc:post',
name: 'post', index: 'post',
className: 'PostService', className: 'PostService',
create: (dtp) => { return new PostService(dtp); }, create: (dtp) => { return new PostService(dtp); },
}; };

@ -118,8 +118,8 @@ class ResourceService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'resource', logId: 'svc:resource',
name: 'resource', index: 'resource',
className: 'ResourceService', className: 'ResourceService',
create: (dtp) => { return new ResourceService(dtp); }, create: (dtp) => { return new ResourceService(dtp); },
}; };

@ -18,7 +18,7 @@ class SessionService extends SiteService {
async start ( ) { async start ( ) {
await super.start(); await super.start();
this.log.info(`starting ${module.exports.name} service`);
passport.serializeUser(this.serializeUser.bind(this)); passport.serializeUser(this.serializeUser.bind(this));
passport.deserializeUser(this.deserializeUser.bind(this)); passport.deserializeUser(this.deserializeUser.bind(this));
} }
@ -110,8 +110,8 @@ class SessionService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'session', logId: 'svc:session',
name: 'session', index: 'session',
className: 'SessionService', className: 'SessionService',
create: (dtp) => { return new SessionService(dtp); }, create: (dtp) => { return new SessionService(dtp); },
}; };

@ -83,8 +83,8 @@ class SiteLinkService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'site-link', logId: 'svc:site-link',
name: 'siteLink', index: 'siteLink',
className: 'SiteLinkService', className: 'SiteLinkService',
create: (dtp) => { return new SiteLinkService(dtp); }, create: (dtp) => { return new SiteLinkService(dtp); },
}; };

@ -18,7 +18,6 @@ class SmsService extends SiteService {
async start ( ) { async start ( ) {
await super.start(); await super.start();
this.log.info(`starting ${module.exports.name} service`);
} }
async stop ( ) { async stop ( ) {
@ -48,8 +47,8 @@ class SmsService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'sms', logId: 'svc:sms',
name: 'sms', index: 'sms',
className: 'SmsService', className: 'SmsService',
create: (dtp) => { return new SmsService(dtp); }, create: (dtp) => { return new SmsService(dtp); },
}; };

@ -11,7 +11,7 @@ const mongoose = require('mongoose');
const Sticker = mongoose.model('Sticker'); const Sticker = mongoose.model('Sticker');
const User = mongoose.model('User'); 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_CHANNEL_STICKERS = 50;
const MAX_USER_STICKERS = 10; const MAX_USER_STICKERS = 10;
@ -144,18 +144,42 @@ class StickerService extends SiteService {
await Sticker.updateOne({ _id: sticker._id }, { $set: { status } }); 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) { async resolveStickerSlugs (slugs) {
const stickers = [ ]; const stickers = await Sticker
await SiteAsync.each(slugs, async (slug) => { .find({ slug: { $in: slugs } })
const sticker = await Sticker.findOne({ slug: slug }); .populate(this.populateSticker)
if (!sticker) { .lean();
return;
}
stickers.push(sticker);
});
return stickers; 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) { async removeSticker (sticker) {
const stickerId = sticker._id; const stickerId = sticker._id;
this.log.info('creating sticker delete job', { stickerId }); this.log.info('creating sticker delete job', { stickerId });
@ -208,8 +232,8 @@ class StickerService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'sticker', logId: 'svc:sticker',
name: 'sticker', index: 'sticker',
className: 'StickerService', className: 'StickerService',
create: (dtp) => { return new StickerService(dtp); }, create: (dtp) => { return new StickerService(dtp); },
}; };

@ -135,8 +135,8 @@ class UserNotificationService extends SiteService {
} }
module.exports = { module.exports = {
name: 'userNotification', logId: 'svc:user-notification',
slug: 'user-notification', index: 'userNotification',
className: 'UserNotificationService', className: 'UserNotificationService',
create: (dtp) => { return new UserNotificationService(dtp); }, create: (dtp) => { return new UserNotificationService(dtp); },
}; };

@ -11,6 +11,7 @@ const mongoose = require('mongoose');
const User = mongoose.model('User'); const User = mongoose.model('User');
const CoreUser = mongoose.model('CoreUser'); const CoreUser = mongoose.model('CoreUser');
const UserBlock = mongoose.model('UserBlock'); const UserBlock = mongoose.model('UserBlock');
const UserArchive = mongoose.model('UserArchive');
const passport = require('passport'); const passport = require('passport');
const PassportLocal = require('passport-local'); const PassportLocal = require('passport-local');
@ -49,13 +50,20 @@ class UserService extends SiteService {
async start ( ) { async start ( ) {
await super.start(); await super.start();
this.log.info(`starting ${module.exports.name} service`);
this.registerPassportLocal(); this.registerPassportLocal();
if (process.env.DTP_ADMIN === 'enabled') { if (process.env.DTP_ADMIN === 'enabled') {
this.registerPassportAdmin(); 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 ( ) { async stop ( ) {
@ -71,6 +79,7 @@ class UserService extends SiteService {
} = this.dtp.services; } = this.dtp.services;
try { try {
this.checkRestrictedKeys('create', userDefinition);
userDefinition.email = userDefinition.email.trim().toLowerCase(); userDefinition.email = userDefinition.email.trim().toLowerCase();
// strip characters we don't want to allow in username // 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'); throw SiteError(403, 'Invalid user account operation');
} }
this.checkRestrictedKeys('create', userDefinition);
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, '')); userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase(); const username_lc = userDefinition.username.toLowerCase();
@ -606,6 +617,24 @@ class UserService extends SiteService {
return users; 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) { async setUserSettings (user, settings) {
const { const {
crypto: cryptoService, crypto: cryptoService,
@ -763,7 +792,18 @@ class UserService extends SiteService {
const { image: imageService } = this.dtp.services; const { image: imageService } = this.dtp.services;
this.log.info('remove profile photo', { user: user._id }); 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) { if (user.picture.large) {
await imageService.deleteImage(user.picture.large); await imageService.deleteImage(user.picture.large);
} }
@ -872,11 +912,14 @@ class UserService extends SiteService {
async ban (user) { async ban (user) {
const { const {
attachment: attachmentService,
chat: chatService, chat: chatService,
comment: commentService, comment: commentService,
contentReport: contentReportService, contentReport: contentReportService,
csrfToken: csrfTokenService, csrfToken: csrfTokenService,
otpAuth: otpAuthService, otpAuth: otpAuthService,
page: pageService,
post: postService,
sticker: stickerService, sticker: stickerService,
userNotification: userNotificationService, userNotification: userNotificationService,
} = this.dtp.services; } = this.dtp.services;
@ -893,12 +936,19 @@ class UserService extends SiteService {
'permissions.canChat': false, 'permissions.canChat': false,
'permissions.canComment': false, 'permissions.canComment': false,
'permissions.canReport': false, 'permissions.canReport': false,
'permissions.canAuthorPages': false,
'permissions.canAuthorPosts': false,
'permissions.canPublishPages': false,
'permissions.canPublishPosts': false,
'optIn.system': false, 'optIn.system': false,
'optIn.marketing': false, 'optIn.marketing': false,
}, },
}, },
); );
await pageService.removeForAuthor(user);
await postService.removeForAuthor(user);
await chatService.removeForUser(user); await chatService.removeForUser(user);
await commentService.removeForAuthor(user); await commentService.removeForAuthor(user);
await contentReportService.removeForUser(user); await contentReportService.removeForUser(user);
@ -906,12 +956,124 @@ class UserService extends SiteService {
await otpAuthService.removeForUser(user); await otpAuthService.removeForUser(user);
await stickerService.removeForUser(user); await stickerService.removeForUser(user);
await userNotificationService.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 = { module.exports = {
slug: 'user', logId: 'svc:user',
name: 'user', index: 'user',
className: 'UserService', className: 'UserService',
create: (dtp) => { return new UserService(dtp); }, create: (dtp) => { return new UserService(dtp); },
}; };

@ -91,7 +91,8 @@ class VenueService extends SiteService {
channel.description = status.description; channel.description = status.description;
await channel.save(); await channel.save();
await this.updateChannelStatus(channel);
channel.currentStatus = status;
return channel.toObject(); return channel.toObject();
} }
@ -105,7 +106,6 @@ class VenueService extends SiteService {
updateOp.$set.slug = this.getChannelSlug(channelDefinition.url); updateOp.$set.slug = this.getChannelSlug(channelDefinition.url);
updateOp.$set.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10); updateOp.$set.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10);
const status = await this.updateChannelStatus(channel);
updateOp.$set.name = status.name; updateOp.$set.name = status.name;
updateOp.$set.description = status.description; updateOp.$set.description = status.description;
@ -120,7 +120,9 @@ class VenueService extends SiteService {
updateOp.$set['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim(); updateOp.$set['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim();
channel = await VenueChannel.findOneAndUpdate({ _id: channel._id }, updateOp, { new: true }); 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) { async getChannels (pagination, options) {
@ -140,7 +142,7 @@ class VenueService extends SiteService {
} }
const channels = await q.populate(this.populateVenueChannel).lean(); 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); channel.currentStatus = await this.updateChannelStatus(channel);
} }
return channels; return channels;
@ -196,20 +198,31 @@ class VenueService extends SiteService {
} }
async updateChannelStatus (channel) { async updateChannelStatus (channel) {
const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`; const { logan: loganService } = this.dtp.services;
this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl }); 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 }); const json = await response.json();
if (!response.ok) { if (!json.success) {
throw new SiteError(500, `Failed to fetch channel status: ${response.statusText}`); throw new Error(`failed to fetch channel status: ${json.message}`);
} }
const json = await response.json(); return json.channel;
if (!json.success) { } catch (error) {
throw new Error(`failed to fetch channel status: ${json.message}`); loganService.sendEvent(module.exports, {
level: 'error',
event: 'updateChannelStatus',
message: error.message,
data: { error },
});
return; // undefined
} }
return json.channel;
} }
getChannelSlug (channelUrl) { getChannelSlug (channelUrl) {
@ -234,8 +247,8 @@ class VenueService extends SiteService {
} }
module.exports = { module.exports = {
slug: 'venue', logId: 'svc:venue',
name: 'venue', index: 'venue',
className: 'VenueService', className: 'VenueService',
create: (dtp) => { return new VenueService(dtp); }, create: (dtp) => { return new VenueService(dtp); },
}; };

@ -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.

@ -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))

@ -32,6 +32,19 @@ ul(uk-nav).uk-nav-default
span.nav-item-icon span.nav-item-icon
i.fas.fa-bullhorn i.fas.fa-bullhorn
span.uk-margin-small-left Announcements 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') }) li(class={ 'uk-active': (adminView === 'post') })
a(href="/admin/post") a(href="/admin/post")
span.nav-item-icon span.nav-item-icon
@ -97,13 +110,18 @@ ul(uk-nav).uk-nav-default
i.fas.fa-user i.fas.fa-user
span.uk-margin-small-left Users 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') }) li(class={ 'uk-active': (adminView === 'content-report') })
a(href="/admin/content-report") a(href="/admin/content-report")
span.nav-item-icon span.nav-item-icon
i.fas.fa-ban i.fas.fa-ban
span.uk-margin-small-left Content Reports span.uk-margin-small-left Content Reports
li.uk-nav-divider li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'core-node') }) li(class={ 'uk-active': (adminView === 'core-node') })

@ -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

@ -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.

@ -2,6 +2,10 @@ extends layouts/main
block content block content
include ../venue/components/channel-grid include ../venue/components/channel-grid
include user/components/list-item
include comment/components/comment
include ../chat/components/message
div(uk-grid) div(uk-grid)
div(class="uk-width-1-1 uk-width-auto@m") div(class="uk-width-1-1 uk-width-auto@m")
@ -30,6 +34,68 @@ block content
if Array.isArray(channels) && (channels.length > 0) if Array.isArray(channels) && (channels.length > 0)
+renderVenueChannelGrid(channels) +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 block viewjs
script(src="/chart.js/chart.min.js") script(src="/chart.js/chart.min.js")
script(src="/chartjs-adapter-moment/chartjs-adapter-moment.min.js") script(src="/chartjs-adapter-moment/chartjs-adapter-moment.min.js")

@ -37,7 +37,7 @@ block content
tr tr
td= moment(log.created).format('YYYY-MM-DD hh:mm:ss.SSS') td= moment(log.created).format('YYYY-MM-DD hh:mm:ss.SSS')
td= log.level td= log.level
td= log.component.slug td= log.component.logId
td td
div= log.message div= log.message
if log.metadata if log.metadata

@ -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

@ -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}.

@ -0,0 +1,7 @@
extends ../../layouts/main
block content
include ../components/list-item
h1 User Archive Job
pre= JSON.stringify(job, null, 2)

@ -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

@ -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

@ -75,6 +75,7 @@ block content
| Can Publish Posts | Can Publish Posts
.uk-margin .uk-margin
- userAccount.optIn = userAccount.optIn || { };
label.uk-form-label Opt-Ins label.uk-form-label Opt-Ins
div(uk-grid).uk-grid-small div(uk-grid).uk-grid-small
label label
@ -93,6 +94,8 @@ block content
div(uk-grid).uk-grid-small div(uk-grid).uk-grid-small
.uk-width-expand .uk-width-expand
+renderBackButton() +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 .uk-width-auto
button(type="submit", name="action", value="ban").uk-button.uk-button-danger.uk-border-rounded Ban User button(type="submit", name="action", value="ban").uk-button.uk-button-danger.uk-border-rounded Ban User
.uk-width-auto .uk-width-auto

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save