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_ACCESS_KEY=dtp-sites
MINIO_SECRET_KEY=
MINIO_ADMIN_BUCKET=site-admin
MINIO_IMAGE_BUCKET=site-images
MINIO_VIDEO_BUCKET=site-videos
MINIO_ATTACHMENT_BUCKET=site-attachments
@ -112,4 +114,14 @@ DTP_LOG_DEBUG=enabled
DTP_LOG_INFO=enabled
DTP_LOG_WARN=enabled
DTP_LOG_HTTP_FORMAT=combined
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,
"unused": true,
"futurehostile": true,
"esversion": 9,
"esversion": 11,
"mocha": true,
"globals": {
"markdown": true,

@ -30,6 +30,27 @@
"console": "integratedTerminal",
"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",
"request": "launch",

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

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

@ -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');
class ContentReportAdminController extends SiteController {
class AdminContentReportController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -87,8 +87,8 @@ class ContentReportAdminController extends SiteController {
}
module.exports = {
name: 'adminContentReport',
slug: 'admin-content-report',
className: 'ContentReportAdminController',
create: async (dtp) => { return new ContentReportAdminController(dtp); },
logId: 'ctl:admin:content-report',
index: 'adminContentReport',
className: 'AdminContentReportController',
create: async (dtp) => { return new AdminContentReportController(dtp); },
};

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

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

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

@ -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');
class JobQueueAdminController extends SiteController {
class AdminJobQueueController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -119,8 +119,8 @@ class JobQueueAdminController extends SiteController {
}
module.exports = {
name: 'adminJobQueue',
slug: 'admin-job-queue',
className: 'JobQueueAdminController',
create: async (dtp) => { return new JobQueueAdminController(dtp); },
logId: 'ctl:admin:job-queue',
index: 'adminJobQueue',
className: 'AdminJobQueueController',
create: async (dtp) => { return new AdminJobQueueController(dtp); },
};

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

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

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

@ -9,14 +9,14 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class OtpAdminController extends SiteController {
class AdminOtpController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
// const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${module.exports.slug}` });
// const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${this.component.logId}` });
const router = express.Router();
router.use(async (req, res, next) => {
@ -49,8 +49,8 @@ class OtpAdminController extends SiteController {
}
module.exports = {
name: 'adminOtp',
slug: 'admin-opt',
className: 'OtpAdminController',
create: async (dtp) => { return new OtpAdminController(dtp); },
logId: 'ctl:admin:otp',
index: 'adminOtp',
className: 'AdminOtpController',
create: async (dtp) => { return new AdminOtpController(dtp); },
};

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

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

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

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

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

@ -8,13 +8,21 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class UserAdminController extends SiteController {
class AdminUserController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const { jobQueue: jobQueueService } = this.dtp.services;
this.jobQueues = { };
this.jobQueues.reeeper = await jobQueueService.getJobQueue(
'reeeper',
this.dtp.config.jobQueues.reeeper,
);
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
@ -23,11 +31,26 @@ class UserAdminController extends SiteController {
});
router.param('localUserId', this.populateLocalUserId.bind(this));
router.param('archiveJobId', this.populateArchiveJobId.bind(this));
router.param('archiveId', this.populateArchiveId.bind(this));
router.post('/local/:localUserId/archive', this.postArchiveLocalUser.bind(this));
router.post('/local/:localUserId', this.postUpdateLocalUser.bind(this));
router.get('/local/:localUserId/archive/confirm', this.getArchiveLocalUserConfirm.bind(this));
router.get('/local/:localUserId', this.getLocalUserView.bind(this));
router.get('/archive/job/:archiveJobId', this.getUserArchiveJobView.bind(this));
router.post('/archive/:archiveId/action', this.postArchiveAction.bind(this));
router.get('/archive/:archiveId/file', this.getUserArchiveFile.bind(this));
router.get('/archive/:archiveId', this.getUserArchiveView.bind(this));
router.get('/archive', this.getUserArchiveIndex.bind(this));
router.get('/', this.getHomeView.bind(this));
return router;
}
@ -44,6 +67,72 @@ class UserAdminController extends SiteController {
}
}
async populateArchiveJobId (req, res, next, archiveJobId) {
try {
res.locals.job = await this.jobQueues.reeeper.getJob(archiveJobId);
if (!res.locals.job) {
throw new SiteError(404, 'Job not found');
}
return next();
} catch (error) {
this.log.error('failed to populate Bull queue job', { archiveJobId, error });
return next(error);
}
}
async populateArchiveId (req, res, next, archiveId) {
const { user: userService } = this.dtp.services;
try {
res.locals.archive = await userService.getArchiveById(archiveId);
if (!res.locals.archive) {
throw new SiteError(404, 'Archive not found');
}
return next();
} catch (error) {
this.log.error('failed to populate UserArchive', { archiveId, error });
return next(error);
}
}
async postArchiveLocalUser (req, res, next) {
const {
logan: loganService,
user: userService,
} = this.dtp.services;
try {
const user = await userService.getLocalUserAccount(req.body.userId);
if (!user) {
throw new SiteError(404, 'User not found');
}
if (req.user && req.user._id.equals(user._id)) {
throw new SiteError(400, "You can't archive yourself");
}
res.locals.job = await userService.archiveLocalUser(user);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postArchiveUser',
data: {
job: res.locals.job.id,
user: user,
},
});
res.redirect(`/admin/user/archive/job/${res.locals.job.id}`);
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postArchiveUser',
data: {
offender: {
_id: req.body.userId,
},
error,
},
});
return next(error);
}
}
async postUpdateLocalUser (req, res, next) {
const {
logan: loganService,
@ -53,6 +142,11 @@ class UserAdminController extends SiteController {
this.log.debug('local user update', { action: req.body.action });
switch (req.body.action) {
case 'update':
if (req.user._id.equals(res.locals.userAccount._id)) {
if (req.user.flags.isAdmin && (req.body.isAdmin !== 'on')) {
throw new SiteError(400, "You can't remove your own admin privileges");
}
}
await userService.updateLocalForAdmin(res.locals.userAccount, req.body);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
@ -68,6 +162,9 @@ class UserAdminController extends SiteController {
break;
case 'ban':
if (req.user._id.equals(res.locals.userAccount._id)) {
throw new SiteError(400, "You can't ban yourself");
}
await userService.ban(res.locals.userAccount);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
@ -100,6 +197,133 @@ class UserAdminController extends SiteController {
}
}
async getUserArchiveJobView (req, res) {
res.locals.adminView = 'user-archive';
res.render('admin/user/archive/job');
}
async getArchiveLocalUserConfirm (req, res) {
res.locals.adminView = 'user-archive';
res.render('admin/user/archive/confirm');
}
async postArchiveAction (req, res, next) {
const {
logan: loganService,
user: userService,
} = this.dtp.services;
try {
switch (req.body.action) {
case 'update':
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postArchiveAction',
message: 'updating user archive record',
data: {
archive: {
_id: res.locals.archive._id,
user: {
_id: res.locals.archive.user._id,
username: res.locals.archive.user.username,
},
},
},
});
await userService.updateArchive(res.locals.archive, req.body);
return res.redirect(`/admin/user/archive/${res.locals.archive._id}`);
case 'delete-file':
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postArchiveAction',
message: 'removing user archive file',
data: {
archive: {
_id: res.locals.archive._id,
user: {
_id: res.locals.archive.user._id,
username: res.locals.archive.user.username,
},
},
},
});
await userService.deleteArchiveFile(res.locals.archive);
return res.redirect(`/admin/user/archive/${res.locals.archive._id}`);
case 'delete':
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postArchiveAction',
message: 'removing user archive',
data: {
archive: {
_id: res.locals.archive._id,
user: {
_id: res.locals.archive.user._id,
username: res.locals.archive.user.username,
},
},
},
});
await userService.deleteArchive(res.locals.archive);
return res.redirect(`/admin/user/archive`);
default:
// unknown/invalid action
break;
}
throw new SiteError(400, `Invalid user archive action: ${req.body.action}`);
} catch (error) {
this.log.error('failed to delete archive file', { error });
return next(error);
}
}
async getUserArchiveFile (req, res, next) {
const { minio: minioService } = this.dtp.services;
try {
res.locals.adminView = 'user-archive';
this.log.debug('archive', { archive: res.locals.archive });
const stream = await minioService.openDownloadStream({
bucket: res.locals.archive.archive.bucket,
key: res.locals.archive.archive.key,
});
res.status(200);
res.set('Content-Type', 'application/zip');
res.set('Content-Size', res.locals.archive.archive.size);
res.set('Content-Disposition', `attachment; filename="user-${res.locals.archive.user._id}.zip"`);
stream.pipe(res);
} catch (error) {
this.log.error('failed to stream user archive file', { error });
return next(error);
}
}
async getUserArchiveView (req, res) {
res.locals.adminView = 'user-archive';
res.render('admin/user/archive/view');
}
async getUserArchiveIndex (req, res, next) {
const { user: userService } = this.dtp.services;
try {
res.locals.adminView = 'user-archive';
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.archive = await userService.getArchives(res.locals.pagination);
res.render('admin/user/archive/index');
} catch (error) {
this.log.error('failed to render the User archives index', { error });
return next(error);
}
}
async getHomeView (req, res, next) {
const { user: userService } = this.dtp.services;
try {
@ -114,8 +338,8 @@ class UserAdminController extends SiteController {
}
module.exports = {
name: 'adminUser',
slug: 'admin-user',
className: 'UserAdminController',
create: async (dtp) => { return new UserAdminController(dtp); },
logId: 'ctl:admin:user',
index: 'adminUser',
className: 'AdminUserController',
create: async (dtp) => { return new AdminUserController(dtp); },
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -17,30 +17,39 @@ class NewsroomController extends SiteController {
async start ( ) {
const { dtp } = this;
const { limiter: limiterService } = dtp.services;
const limiterConfig = limiterService.config.newsroom;
const router = express.Router();
dtp.app.use('/newsroom', router);
router.use(async (req, res, next) => {
res.locals.currentView = module.exports.slug;
res.locals.currentView = module.exports.logId;
return next();
});
router.param('feedId', this.populateFeedId.bind(this));
router.get('/feed',
limiterService.createMiddleware(limiterConfig.getUnifiedFeed),
this.getUnifiedFeed.bind(this),
);
router.get('/:feedId',
limiterService.createMiddleware(limiterService.config.newsroom.getFeedView),
limiterService.createMiddleware(limiterConfig.getFeedView),
this.getFeedView.bind(this),
);
router.get('/',
limiterService.createMiddleware(limiterService.config.newsletter.getIndex),
limiterService.createMiddleware(limiterConfig.getIndex),
this.getHome.bind(this),
);
}
async populateFeedId (req, res, next, feedId) {
const { feed: feedService } = this.dtp.services;
const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.feed = await feedService.getById(feedId);
if (!res.locals.feed) {
@ -48,39 +57,116 @@ class NewsroomController extends SiteController {
}
return next();
} catch (error) {
this.log.error('failed to populate feedId', { feedId, error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateFeedId',
message: error.message,
data: { feedId, error },
});
return next(error);
}
}
async getUnifiedFeed (req, res) {
const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.newsroom = await feedService.getNewsfeed(res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getUnifiedFeed',
data: { fmt: req.query.fmt || 'html' },
});
switch (req.query.fmt) {
case 'json':
res.status(200).json(res.locals.newsroom);
break;
default:
res.render('newsroom/unified-feed');
break;
}
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getUnifiedFeed',
message: error.message,
data: { error },
});
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async getFeedView (req, res, next) {
const { feed: feedService } = this.dtp.services;
const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.newsroom = await feedService.getFeedEntries(res.locals.feed, res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getFeedView',
data: {
feed: {
_id: res.locals.feed._id,
title: res.locals.feed.title,
},
},
});
res.render('newsroom/feed-view');
} catch (error) {
this.log.error('failed to present newsroom home', { error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getFeedView',
message: error.message,
data: { error },
});
return next(error);
}
}
async getHome (req, res, next) {
const { feed: feedService } = this.dtp.services;
const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 12);
res.locals.newsroom = await feedService.getFeeds(res.locals.pagination, { withEntries: true });
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHome',
});
res.render('newsroom/index');
} catch (error) {
this.log.error('failed to present newsroom home', { error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getHome',
message: error.message,
data: { error },
});
return next(error);
}
}
}
module.exports = {
slug: 'newsroom',
name: 'newsroom',
logId: 'ctl:newsroom',
index: 'newsroom',
className: 'NewsroomController',
create: (dtp) => { return new NewsroomController(dtp); },
};

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

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

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

@ -119,13 +119,13 @@ class UserController extends SiteController {
);
router.get(
'/:userId/otp-setup',
'/:localUserId/otp-setup',
limiterService.createMiddleware(limiterService.config.user.getOtpSetup),
otpSetup,
this.getOtpSetup.bind(this),
);
router.get(
'/:userId/otp-disable',
'/:localUserId/otp-disable',
limiterService.createMiddleware(limiterService.config.user.getOtpDisable),
authRequired,
this.getOtpDisable.bind(this),
@ -148,7 +148,7 @@ class UserController extends SiteController {
);
router.delete(
'/:userId/profile-photo',
'/:localUserId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto),
authRequired,
checkProfileOwner,
@ -334,6 +334,12 @@ class UserController extends SiteController {
});
} catch (error) {
this.log.error('failed to create new user', { error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postCreateUser',
message: `failed to create user account: ${error.message}`,
data: { definition: req.body, error },
});
return next(error);
}
}
@ -639,8 +645,8 @@ class UserController extends SiteController {
}
module.exports = {
slug: 'user',
name: 'user',
logId: 'ctl:user',
index: 'user',
className: 'UserController',
create: async (dtp) => { return new UserController(dtp); },
};
};

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

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

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

@ -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 = {
slug: 'announcement',
name: 'announcement',
logId: 'svc:announcement',
index: 'announcement',
className: 'AnnouncementService',
create: (dtp) => { return new AnnouncementService(dtp); },
};

@ -126,6 +126,23 @@ class AttachmentService extends SiteService {
return attachments;
}
/**
* Access all attachments sorted by most recent with pagination. This is for
* use by Admin tools.
* @param {*} pagination required pagination parameters (skip and cpp)
* @returns Array of attachments
*/
async getRecent (pagination) {
const attachments = await Attachment
.find()
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateAttachment)
.lean();
return attachments;
}
/**
*
* @param {mongoose.Types.ObjectId} attachmentId The ID of the attachment
@ -166,6 +183,20 @@ class AttachmentService extends SiteService {
return this.attachmentTemplate({ attachment, attachmentOptions });
}
/**
* Removes all attachments and everything on storage about them for a
* specified User.
* @param {User} owner the owner of the attachments to be removed
*/
async removeForOwner (owner) {
const handler = this.remove.bind(this);
await Attachment
.find({ owner: owner._id })
.lean()
.cursor()
.eachAsync(handler);
}
/**
* Creates a Bull Queue job to delete an Attachment including it's processed
* and original media files.
@ -180,8 +211,8 @@ class AttachmentService extends SiteService {
}
module.exports = {
slug: 'attachment',
name: 'attachment',
logId: 'svc:attachment',
index: 'attachment',
className: 'AttachmentService',
create: (dtp) => { return new AttachmentService(dtp); },
};

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

@ -785,6 +785,16 @@ class ChatService extends SiteService {
return reaction.toObject();
}
async getRecent (limit = 10) {
const messages = await ChatMessage
.find({ })
.sort({ created: -1 })
.limit(limit)
.populate(this.populateChatMessage)
.lean();
return messages;
}
async removeForUser (user) {
const { logan: loganService } = this.dtp.services;
@ -852,8 +862,8 @@ class ChatService extends SiteService {
}
module.exports = {
slug: 'chat',
name: 'chat',
logId: 'svc:chat',
index: 'chat',
className: 'ChatService',
create: (dtp) => { return new ChatService(dtp); },
};

@ -251,6 +251,21 @@ class CommentService extends SiteService {
return comments;
}
/**
* Meant for use in admin tools.
*/
async getRecent (pagination) {
const comments = await Comment
.find()
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateComment)
.lean();
const totalCommentCount = await Comment.estimatedDocumentCount();
return { comments, totalCommentCount };
}
async getForAuthor (author, pagination) {
const comments = await Comment
.find({ // index: 'comment_author_by_type'
@ -335,8 +350,8 @@ class CommentService extends SiteService {
}
module.exports = {
slug: 'comment',
name: 'comment',
logId: 'svc:comment',
index: 'comment',
className: 'CommentService',
create: (dtp) => { return new CommentService(dtp); },
};

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

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

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

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

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

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

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

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

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

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

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

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

@ -90,16 +90,37 @@ class ImageService extends SiteService {
return image;
}
async getRecentImagesForOwner(owner) {
async getRecentImagesForOwner(owner, limit = 10) {
const images = await SiteImage
.find({ owner: owner._id })
.sort({ created: -1 })
.limit(10)
.limit(limit)
.populate(this.populateImage)
.lean();
return images;
}
async getRecentImages (pagination) {
const images = await SiteImage
.find()
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateImage)
.lean();
const totalImageCount = await SiteImage.estimatedDocumentCount();
return { images, totalImageCount };
}
async downloadImage (image, filename) {
const { minio: minioService } = this.dtp.services;
return minioService.downloadFile({
bucket: image.file.bucket,
key: image.file.key,
filePath: filename,
});
}
async deleteImage(image) {
const { minio: minioService } = this.dtp.services;
@ -350,8 +371,8 @@ class ImageService extends SiteService {
}
module.exports = {
slug: 'image',
name: 'image',
logId: 'svc:image',
index: 'image',
className: 'ImageService',
create: (dtp) => { return new ImageService(dtp); },
};

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

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

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

@ -4,9 +4,7 @@
'use strict';
const os = require('os');
const { SiteService, SiteError } = require('../../lib/site-lib');
const { SiteService } = require('../../lib/site-lib');
class LoganService extends SiteService {
@ -16,64 +14,62 @@ class LoganService extends SiteService {
async start ( ) {
await super.start();
}
async sendRequestEvent (component, req, event) {
if (process.env.DTP_LOGAN !== 'enabled') {
return;
}
if (req.user) {
event.data = event.data || { };
event.data.user = {
_id: req.user._id,
username: req.user.username,
};
}
event.ip = req.ip;
return this.sendEvent(component, event);
}
async sendEvent (component, event) {
if (process.env.DTP_LOGAN !== 'enabled') {
return;
}
try {
const loganScheme = process.env.DTP_LOGAN_SCHEME || 'http';
const loganUrl = `${loganScheme}://${process.env.DTP_LOGAN_HOST}/api/event`;
event.host = os.hostname();
event['component.slug'] = component.slug;
event['component.name'] = component.className || component.name;
const { LoganClient } = await import('dtp-logan-api');
this.log[event.level]('sending Logan event', { event });
this.log.info('creating Logan client');
this.logan = new LoganClient();
const payload = JSON.stringify(event);
const response = await fetch(loganUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': payload.length,
'X-LogAn-Auth': process.env.DTP_LOGAN_API_KEY,
this.log.info('initializing Logan client');
await this.logan.initialize({
log: this.log,
api: {
enabled: process.env.DTP_LOGAN === 'enabled',
key: process.env.DTP_LOGAN_API_KEY,
scheme: process.env.DTP_LOGAN_SCHEME,
host: process.env.DTP_LOGAN_HOST,
},
request: {
userField: 'user',
userIdField: '_id',
usernameField: 'username',
},
flags: {
includeHostname: true,
includeClientIpAddress: true,
includeUser: true,
},
queue: {
enabled: true,
name: process.env.DTP_LOGAN_QUEUE_NAME || 'logan',
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
username: process.env.REDIS_USERNAME, // requires Redis >= 6
password: process.env.REDIS_PASSWORD,
keyPrefix: process.env.REDIS_PREFIX,
},
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: true,
},
body: payload,
});
},
});
}
const json = await response.json();
if (!json.success) {
throw new SiteError(500, json.message);
}
async sendRequestEvent (component, req, event) {
return this.logan.sendRequestEvent(component, req, event);
}
return json;
} catch (error) {
this.log.error('failed to send LOGAN event', { event, error });
// fall through
}
async sendEvent (component, event) {
return this.logan.sendEvent(component, event);
}
}
module.exports = {
slug: 'logan',
name: 'logan',
logId: 'svc:logan',
index: 'logan',
className: 'LoganService',
create: (dtp) => { return new LoganService(dtp); },
};

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

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

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

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

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

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

@ -7,7 +7,7 @@
const striptags = require('striptags');
const slug = require('slug');
const { SiteService, SiteError, SiteAsync } = require('../../lib/site-lib');
const { SiteService, SiteError } = require('../../lib/site-lib');
const mongoose = require('mongoose');
const ObjectId = mongoose.Types.ObjectId;
@ -180,7 +180,6 @@ class PageService extends SiteService {
return this.getById(pageId);
}
async isParentPage (page) {
if (page) {
page = [page];
@ -204,9 +203,37 @@ class PageService extends SiteService {
return pages;
}
async deletePage (page) {
this.log.info('deleting page', { pageId: page._id });
async deletePage (page, options) {
options = Object.assign({ updateCache: true }, options);
this.log.info('deleting page', { pageId: page._id, options });
await Page.deleteOne({ _id: page._id });
if (options.updateCache) {
await this.cacheMainMenuPages();
}
}
async removeForAuthor (author) {
/*
* Execute the updates without page cache updates
*/
await Page
.find({ author: author._id })
.cursor()
.eachAsync(async (page) => {
try {
await this.deletePage(page, { updateCache: false });
} catch (error) {
this.log.error('failed to remove page for author', { error });
// fall through
}
});
/*
* and update the page cache once, instead.
*/
await this.cacheMainMenuPages();
}
createPageSlug (pageId, pageTitle) {
@ -217,90 +244,92 @@ class PageService extends SiteService {
return `${pageSlug}-${pageId}`;
}
async cacheMainMenuPages () {
try {
const pages = await Page
.find({ status: 'published' })
.select('slug menu')
.populate({path: 'menu.parent'})
.lean();
async cacheMainMenuPages ( ) {
let mainMenu = [];
await SiteAsync.each(pages, async (page) => {
if (page.menu.parent) {
let parent = page.menu.parent;
if (parent.status === 'published') {
let parentPage = mainMenu.find(item => item.slug === parent.slug);
if (parentPage) {
let childPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
};
parentPage.children.splice(childPage.order, 0, childPage);
}
else {
let parentPage = {
url: `/page/${parent.slug}`,
slug: parent.slug,
icon: parent.menu.icon,
label: parent.menu.label,
order: parent.menu.order,
children: [],
};
let childPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
};
parentPage.children.splice(childPage.order, 0, childPage);
mainMenu.splice(parentPage.order, 0, parentPage);
try {
await Page
.find({ status: 'published' })
.select('slug menu')
.populate({path: 'menu.parent'})
.lean()
.cursor()
.eachAsync(async (page) => {
if (page.menu.parent) {
let parent = page.menu.parent;
if (parent.status === 'published') {
let parentPage = mainMenu.find(item => item.slug === parent.slug);
if (parentPage) {
let childPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
};
parentPage.children.splice(childPage.order, 0, childPage);
}
else {
let parentPage = {
url: `/page/${parent.slug}`,
slug: parent.slug,
icon: parent.menu.icon,
label: parent.menu.label,
order: parent.menu.order,
children: [],
};
let childPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
};
parentPage.children.splice(childPage.order, 0, childPage);
mainMenu.splice(parentPage.order, 0, parentPage);
}
} else {
let menuPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
children: [],
};
mainMenu.splice(menuPage.order, 0, menuPage);
}
} else {
let isPageInMenu = mainMenu.find(item => item.slug === page.slug);
if (!isPageInMenu) {
let menuPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
children: [],
};
mainMenu.push(menuPage);
}
}
} else {
let menuPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
children: [],
};
mainMenu.splice(menuPage.order, 0, menuPage);
}
} else {
let isPageInMenu = mainMenu.find(item => item.slug === page.slug);
if (!isPageInMenu) {
let menuPage = {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
children: [],
};
mainMenu.push(menuPage);
});
/*
* Sort the menu data
*/
mainMenu.sort((a, b) => a.order - b.order);
for (const menu of mainMenu) {
if (menu.children) {
menu.children.sort((a, b) => a.order - b.order);
}
}
});
mainMenu.sort((a, b) => a.order - b.order);
await SiteAsync.each(mainMenu, async (menu) => {
if (menu.children) {
menu.children.sort((a, b) => a.order - b.order);
}
});
const deleteResponse = await this.dtp.services.cache.del("mainMenu");
this.dtp.log.info(deleteResponse);
const storeResponse = await this.dtp.services.cache.setObject("mainMenu", mainMenu);
this.dtp.log.info(storeResponse);
// const getresp = await this.dtp.services.cache.getObject("mainMenu");
/*
* Update the cache
*/
await this.dtp.services.cache.setObject("mainMenu", mainMenu);
} catch (error) {
this.dtp.log.error('failed to build page menu', { error });
}
@ -313,8 +342,8 @@ class PageService extends SiteService {
module.exports = {
slug: 'page',
name: 'page',
logId: 'svc:page',
index: 'page',
className: 'PageService',
create: (dtp) => { return new PageService(dtp); },
};
};

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

@ -400,6 +400,23 @@ class PostService extends SiteService {
await Post.deleteOne({ _id: post._id });
}
async removeForAuthor (author) {
await Post
.find({
authorType: author.type,
author: author._id,
})
.cursor()
.eachAsync(async (post) => {
try {
await this.deletePost(post);
} catch (error) {
this.log.error('failed to remove post for author', { error });
// fall through
}
});
}
createPostSlug (postId, postTitle) {
if ((typeof postTitle !== 'string') || (postTitle.length < 1)) {
throw new Error('Invalid input for making a post slug');
@ -410,8 +427,8 @@ class PostService extends SiteService {
}
module.exports = {
slug: 'post',
name: 'post',
logId: 'svc:post',
index: 'post',
className: 'PostService',
create: (dtp) => { return new PostService(dtp); },
};

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

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

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

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

@ -11,7 +11,7 @@ const mongoose = require('mongoose');
const Sticker = mongoose.model('Sticker');
const User = mongoose.model('User');
const { SiteService, SiteError, SiteAsync } = require('../../lib/site-lib');
const { SiteService, SiteError } = require('../../lib/site-lib');
const MAX_CHANNEL_STICKERS = 50;
const MAX_USER_STICKERS = 10;
@ -144,18 +144,42 @@ class StickerService extends SiteService {
await Sticker.updateOne({ _id: sticker._id }, { $set: { status } });
}
/**
* Fetch and populate an Array of Sticker documents matching the input slugs.
*
* The returned Array may be smaller than the input Array (or empty) if some
* or none of the stickers have been deleted or simply don't exist.
*
* @param {Array} slugs an Array of sticker slugs
* @returns Array of populated matching Sticker documents
*/
async resolveStickerSlugs (slugs) {
const stickers = [ ];
await SiteAsync.each(slugs, async (slug) => {
const sticker = await Sticker.findOne({ slug: slug });
if (!sticker) {
return;
}
stickers.push(sticker);
});
const stickers = await Sticker
.find({ slug: { $in: slugs } })
.populate(this.populateSticker)
.lean();
return stickers;
}
/**
* Convert an array of sticker slugs to an array of sticker _id values. This
* is used during chat message ingest where the whole populated sticker just
* isn't needed and is all discarded.
*
* The returned Array may be smaller than the input Array (or empty) if some
* or none of the stickers have been deleted or simply don't exist.
*
* @param {Array} slugs an Array of sticker slugs
* @returns an Array of sticker IDs
*/
async resolveStickerSlugIds (slugs) {
const stickers = await Sticker
.find({ slug: { $in: slugs } })
.select('_id')
.lean();
return stickers.map((sticker) => sticker._id);
}
async removeSticker (sticker) {
const stickerId = sticker._id;
this.log.info('creating sticker delete job', { stickerId });
@ -208,8 +232,8 @@ class StickerService extends SiteService {
}
module.exports = {
slug: 'sticker',
name: 'sticker',
logId: 'svc:sticker',
index: 'sticker',
className: 'StickerService',
create: (dtp) => { return new StickerService(dtp); },
};

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

@ -11,6 +11,7 @@ const mongoose = require('mongoose');
const User = mongoose.model('User');
const CoreUser = mongoose.model('CoreUser');
const UserBlock = mongoose.model('UserBlock');
const UserArchive = mongoose.model('UserArchive');
const passport = require('passport');
const PassportLocal = require('passport-local');
@ -49,13 +50,20 @@ class UserService extends SiteService {
async start ( ) {
await super.start();
this.log.info(`starting ${module.exports.name} service`);
this.registerPassportLocal();
if (process.env.DTP_ADMIN === 'enabled') {
this.registerPassportAdmin();
}
const { jobQueue: jobQueueService } = this.dtp.services;
this.jobQueues = { };
this.log.info('connecting to job queue', { name: 'reeeper', config: this.dtp.config.jobQueues.reeeper });
this.jobQueues.reeeper = jobQueueService.getJobQueue(
'reeeper',
this.dtp.config.jobQueues.reeeper,
);
}
async stop ( ) {
@ -71,6 +79,7 @@ class UserService extends SiteService {
} = this.dtp.services;
try {
this.checkRestrictedKeys('create', userDefinition);
userDefinition.email = userDefinition.email.trim().toLowerCase();
// strip characters we don't want to allow in username
@ -215,6 +224,8 @@ class UserService extends SiteService {
throw SiteError(403, 'Invalid user account operation');
}
this.checkRestrictedKeys('create', userDefinition);
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -606,6 +617,24 @@ class UserService extends SiteService {
return users;
}
async getAdmins ( ) {
const admins = await User
.find({ 'flags.isAdmin': true })
.select(UserService.USER_SELECT)
.sort({ username: 1 })
.lean();
return admins;
}
async getModerators ( ) {
const moderators = await User
.find({ 'flags.isModerator': true })
.select(UserService.USER_SELECT)
.sort({ username: 1 })
.lean();
return moderators;
}
async setUserSettings (user, settings) {
const {
crypto: cryptoService,
@ -763,7 +792,18 @@ class UserService extends SiteService {
const { image: imageService } = this.dtp.services;
this.log.info('remove profile photo', { user: user._id });
user = await this.getUserAccount(user._id);
switch (user.type) {
case 'User':
user = await this.getLocalUserAccount(user._id);
break;
case 'CoreUser':
user = await this.getCoreUserAccount(user._id);
break;
default:
throw new SiteError(400, 'Invalid User type');
}
if (user.picture.large) {
await imageService.deleteImage(user.picture.large);
}
@ -872,11 +912,14 @@ class UserService extends SiteService {
async ban (user) {
const {
attachment: attachmentService,
chat: chatService,
comment: commentService,
contentReport: contentReportService,
csrfToken: csrfTokenService,
otpAuth: otpAuthService,
page: pageService,
post: postService,
sticker: stickerService,
userNotification: userNotificationService,
} = this.dtp.services;
@ -893,12 +936,19 @@ class UserService extends SiteService {
'permissions.canChat': false,
'permissions.canComment': false,
'permissions.canReport': false,
'permissions.canAuthorPages': false,
'permissions.canAuthorPosts': false,
'permissions.canPublishPages': false,
'permissions.canPublishPosts': false,
'optIn.system': false,
'optIn.marketing': false,
},
},
);
await pageService.removeForAuthor(user);
await postService.removeForAuthor(user);
await chatService.removeForUser(user);
await commentService.removeForAuthor(user);
await contentReportService.removeForUser(user);
@ -906,12 +956,124 @@ class UserService extends SiteService {
await otpAuthService.removeForUser(user);
await stickerService.removeForUser(user);
await userNotificationService.removeForUser(user);
await attachmentService.removeForOwner(user);
}
checkRestrictedKeys (method, definition) {
const { logan: loganService } = this.dtp.services;
const restrictedKeys = [
'isAdmin', 'isModerator', 'isEmailVerified',
'canLogin', 'canChat', 'canComment', 'canReport',
'optInSystem', 'optInMarketing',
];
const keys = Object.keys(definition);
for (const restrictedKey of restrictedKeys) {
if (keys.includes(restrictedKey)) {
loganService.sendEvent(module.exports, {
level: 'alert',
event: method,
message: 'malicious fields detected',
data: { definition },
});
throw new SiteError(403, 'invalid request');
}
}
}
/**
* Create a job to archive and ban a User (local). The job will immediately
* disable the specified user, create a .zip file of their content on storage.
* Once the worker confirms that the archive file is on storage, it creates a
* UserArchive record for it, then completely bans the User. That removes all
* of the User's content.
*
* It then removes the User record entirely.
*
* @param {User} user the User to be archived
* @returns the newly created Bull queue job
*/
async archiveLocalUser (user) {
return this.jobQueues.reeeper.add('archive-user-local', { userId: user._id });
}
/**
* Update a UserArchive document
* @param {UserArchive} archive the existing archive to be updated
* @param {*} archiveDefinition new values to be applied
*/
async updateArchive (archive, archiveDefinition) {
const update = { $set: { }, $unset: { } };
archiveDefinition.notes = archiveDefinition.notes.trim();
if (archiveDefinition.notes && (archiveDefinition.notes.length > 0)) {
update.$set.notes = archiveDefinition.notes;
} else {
update.$unset.notes = 1;
}
await UserArchive.updateOne({ _id: archive._id }, update);
}
/**
* Fetch an Array of UserArchive documents with pagination.
* @param {DtpPagination} pagination self explanatory
* @returns Array of UserArchive documents (can be empty)
*/
async getArchives (pagination) {
const search = { };
const archives = await UserArchive
.find(search)
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.lean();
const totalArchiveCount = await UserArchive.estimatedDocumentCount();
return { archives, totalArchiveCount };
}
/**
* Fetch a UserArchive record. This does not fetch the archive file.
* @param {UserArchive} archiveId the ID of the archive to fetch
* @returns the requested UserArchive, or null/undefined.
*/
async getArchiveById (archiveId) {
const archive = await UserArchive.findOne({ _id: archiveId }).lean();
return archive;
}
/**
* Removes the .zip file attached to a UserArchive.
* @param {UserArchive} archive the archive for which an associated .zip file
* is to be removed
*/
async deleteArchiveFile (archive) {
const { minio: minioService } = this.dtp.services;
if (!archive.archive || !archive.archive.bucket || !archive.archive.key) {
return; // no archive file present, abort
}
await minioService.removeObject(archive.archive.bucket, archive.archive.key);
await UserArchive.updateOne(
{ _id: archive._id },
{
$unset: { archive: 1 },
},
);
}
/**
* Removes a UserArchive and any attached data.
* @param {UserArchive} archive the UserArchive to be removed.
*/
async deleteArchive (archive) {
await this.deleteArchiveFile(archive);
await UserArchive.deleteOne({ _id: archive._id });
}
}
module.exports = {
slug: 'user',
name: 'user',
logId: 'svc:user',
index: 'user',
className: 'UserService',
create: (dtp) => { return new UserService(dtp); },
};

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

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

@ -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
include ../venue/components/channel-grid
include user/components/list-item
include comment/components/comment
include ../chat/components/message
div(uk-grid)
div(class="uk-width-1-1 uk-width-auto@m")
@ -30,6 +34,68 @@ block content
if Array.isArray(channels) && (channels.length > 0)
+renderVenueChannelGrid(channels)
.uk-margin
div(uk-grid)
div(class="uk-width-1-1 uk-width-1-3@l")
h3 Admins
if Array.isArray(admins) && (admins.length > 0)
ul.uk-list.uk-list-divider
each member in admins
li
+renderUserListItem(member)
else
div There are no system admins.
h3 Moderators
if Array.isArray(moderators) && (moderators.length > 0)
ul.uk-list.uk-list-divider
each member in moderators
li
+renderUserListItem(member)
else
div There are no system-level moderators.
h3 Recent Members
if Array.isArray(recentMembers) && (recentMembers.length > 0)
ul.uk-list.uk-list-divider
each member in recentMembers
li
+renderUserListItem(member)
else
div There are no recent members.
div(class="uk-width-1-1 uk-width-1-3@l")
h3 Recent Chat
if Array.isArray(recentChat) && (recentChat.length > 0)
ul.uk-list.uk-list-divider
each message in recentChat
li
div(uk-grid).uk-grid-small
.uk-width-expand
+renderChatMessage(message, { fullWidth: true })
.uk-width-auto
a(href=`/admin/user/local/${message.author._id}`, uk-tooltip={ title: 'Manage user account' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded
span
i.fas.fa-wrench
else
div There is no recent chat.
div(class="uk-width-1-1 uk-width-1-3@l")
h3 Recent Comments
if Array.isArray(recentComments.comments) && (recentComments.comments.length > 0)
ul.uk-list.uk-list-divider
each comment in recentComments.comments
li
div(uk-grid).uk-grid-small
.uk-width-expand
+renderComment(comment)
.uk-width-auto
a(href=`/admin/user/local/${comment.author._id}`, uk-tooltip={ title: 'Manage user account' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded
span
i.fas.fa-wrench
else
div There are no recent comments.
block viewjs
script(src="/chart.js/chart.min.js")
script(src="/chartjs-adapter-moment/chartjs-adapter-moment.min.js")

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

@ -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
.uk-margin
- userAccount.optIn = userAccount.optIn || { };
label.uk-form-label Opt-Ins
div(uk-grid).uk-grid-small
label
@ -93,6 +94,8 @@ block content
div(uk-grid).uk-grid-small
.uk-width-expand
+renderBackButton()
.uk-width-auto
a(href=`/admin/user/local/${userAccount._id}/archive/confirm`).uk-button.uk-button-danger.uk-border-rounded Archive User
.uk-width-auto
button(type="submit", name="action", value="ban").uk-button.uk-button-danger.uk-border-rounded Ban User
.uk-width-auto

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

Loading…
Cancel
Save