Merge branch 'master' of git.digitaltelepresence.com:digital-telepresence/dtp-sites

master
Andrew 1 year ago
commit ee6224202d

@ -62,6 +62,9 @@ MAILGUN_DOMAIN=
MONGODB_HOST=localhost:27017 MONGODB_HOST=localhost:27017
MONGODB_DATABASE=dtp-sites MONGODB_DATABASE=dtp-sites
MONGODB_USERNAME=mongo-user
MONGODB_PASSWORD=change-me!
MONGODB_OPTIONS=
# #
# Redis configuration # Redis configuration

@ -12,6 +12,15 @@ The only qualified operated system for hosting a DTP Sites suite is [Ubuntu 20.0
apt-get -y install build-essential python3-pip apt-get -y install build-essential python3-pip
``` ```
## Host Preparation
The following commands must be exeucted on any host expected to run DTP Framework applications.
```sh
apt -y update && apt -y upgrade
apt -y install linux-headers-generic linux-headers-virtual linux-image-virtual linux-virtual
apt -y install build-essential ffmpeg supervisor
```
## Install Data Tier Components ## Install Data Tier Components
You will need MongoDB and MinIO installed and running before you can start DTP Sites web services. You will need MongoDB and MinIO installed and running before you can start DTP Sites web services.

@ -81,6 +81,7 @@ class AdminController extends SiteController {
coreNode: coreNodeService, coreNode: coreNodeService,
dashboard: dashboardService, dashboard: dashboardService,
venue: venueService, venue: venueService,
logan: loganService,
} = this.dtp.services; } = this.dtp.services;
res.locals.stats = { res.locals.stats = {
@ -92,6 +93,11 @@ class AdminController extends SiteController {
res.locals.channels = await venueService.getChannels(); res.locals.channels = await venueService.getChannels();
res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`; res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHomeView',
});
res.render('admin/index'); res.render('admin/index');
} }
} }
@ -99,5 +105,6 @@ class AdminController extends SiteController {
module.exports = { module.exports = {
slug: 'admin', slug: 'admin',
name: 'admin', name: 'admin',
className: 'AdminController',
create: async (dtp) => { return new AdminController(dtp); }, create: async (dtp) => { return new AdminController(dtp); },
}; };

@ -38,31 +38,76 @@ class AnnouncementAdminController extends SiteController {
} }
async populateAnnouncementId (req, res, next, announcementId) { async populateAnnouncementId (req, res, next, announcementId) {
const { announcement: announcementService } = this.dtp.services; const {
announcement: announcementService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.announcement = await announcementService.getById(announcementId); res.locals.announcement = await announcementService.getById(announcementId);
return next(); return next();
} catch (error) { } catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateAnnouncementId',
message: `failed to populate announcement: ${error.message}`,
data: { announcementId, error },
});
return next(error); return next(error);
} }
} }
async postUpdateAnnouncement (req, res, next) { async postUpdateAnnouncement (req, res, next) {
const { announcement: announcementService } = this.dtp.services; const {
announcement: announcementService,
logan: loganService,
} = this.dtp.services;
try { try {
await announcementService.update(res.locals.announcement, req.body); await announcementService.update(res.locals.announcement, req.body);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postUpdateAnnouncement',
message: 'announcement updated',
data: {
announcement: {
_id: res.locals.announcement._id,
},
},
});
res.redirect('/admin/announcement'); res.redirect('/admin/announcement');
} catch (error) { } catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postUpdateAnnouncement',
message: `failed to update announcement: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async postCreateAnnouncement (req, res, next) { async postCreateAnnouncement (req, res, next) {
const { announcement: announcementService } = this.dtp.services; const {
announcement: announcementService,
logan: loganService,
} = this.dtp.services;
try { try {
await announcementService.create(req.body); res.locals.announcement = await announcementService.create(req.body);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postCreateAnnouncement',
message: 'announcement created',
data: {
announcement: res.locals.announcement,
},
});
res.redirect('/admin/announcement'); res.redirect('/admin/announcement');
} catch (error) { } catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postCreateAnnouncement',
message: `failed to create announcement: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
@ -83,14 +128,27 @@ class AnnouncementAdminController extends SiteController {
} }
async deleteAnnouncement (req, res) { async deleteAnnouncement (req, res) {
const { announcement: announcementService } = this.dtp.services; const {
announcement: announcementService,
logan: loganService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('delete-announcement'); const displayList = this.createDisplayList('delete-announcement');
await announcementService.remove(res.locals.announcement); await announcementService.remove(res.locals.announcement);
displayList.reload(); displayList.reload();
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'deleteAnnouncement',
data: { announcement: { _id: res.locals.announcement._id } },
});
} catch (error) { } catch (error) {
this.log.error('failed to delete announcement', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'deleteAnnouncement',
message: `failed to delete announcement: ${error.message}`,
data: { error },
});
res.status(error.statusCode || 500).json({ res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -102,8 +160,6 @@ class AnnouncementAdminController extends SiteController {
module.exports = { module.exports = {
name: 'announcement', name: 'announcement',
slug: 'announcement', slug: 'announcement',
create: async (dtp) => { className: 'AnnouncementAdminController',
let controller = new AnnouncementAdminController(dtp); create: async (dtp) => { return new AnnouncementAdminController(dtp); },
return controller;
},
}; };

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

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

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

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

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

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController } = require('../../../lib/site-lib');
class LogController extends SiteController { class LogAdminController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -53,5 +53,6 @@ class LogController extends SiteController {
module.exports = { module.exports = {
name: 'adminLog', name: 'adminLog',
slug: 'admin-log', slug: 'admin-log',
create: async (dtp) => { return new LogController(dtp); }, className: 'LogAdminController',
create: async (dtp) => { return new LogAdminController(dtp); },
}; };

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

@ -160,5 +160,6 @@ class NewsroomAdminController extends SiteController {
module.exports = { module.exports = {
name: 'newsroomAdmin', name: 'newsroomAdmin',
slug: 'newsroom-admin', slug: 'newsroom-admin',
className: 'NewsroomAdminController',
create: async (dtp) => { return new NewsroomAdminController(dtp); }, create: async (dtp) => { return new NewsroomAdminController(dtp); },
}; };

@ -51,5 +51,6 @@ class OtpAdminController extends SiteController {
module.exports = { module.exports = {
name: 'adminOtp', name: 'adminOtp',
slug: 'admin-opt', slug: 'admin-opt',
className: 'OtpAdminController',
create: async (dtp) => { return new OtpAdminController(dtp); }, create: async (dtp) => { return new OtpAdminController(dtp); },
}; };

@ -40,10 +40,12 @@ class PageController extends SiteController {
async populatePageId (req, res, next, pageId) { async populatePageId (req, res, next, pageId) {
const { page: pageService } = this.dtp.services; const { page: pageService } = this.dtp.services;
try { try {
res.locals.page = await pageService.getById(pageId); res.locals.page = await pageService.getById(pageId, true);
if (!res.locals.page) { if (!res.locals.page) {
throw new SiteError(404, 'Page not found'); throw new SiteError(404, 'Page not found');
} }
res.locals.isParentPage = await pageService.isParentPage(pageId);
return next(); return next();
} catch (error) { } catch (error) {
this.log.error('failed to populate pageId', { pageId, error }); this.log.error('failed to populate pageId', { pageId, error });
@ -131,5 +133,6 @@ class PageController extends SiteController {
module.exports = { module.exports = {
name: 'adminPage', name: 'adminPage',
slug: 'admin-page', slug: 'admin-page',
className: 'PageController',
create: async (dtp) => { return new PageController(dtp); }, create: async (dtp) => { return new PageController(dtp); },
}; };

@ -101,7 +101,9 @@ class PostController extends SiteController {
try { try {
res.locals.pageTitle = `Posts for ${this.dtp.config.site.name}`; res.locals.pageTitle = `Posts for ${this.dtp.config.site.name}`;
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getAllPosts(res.locals.pagination); const {posts, totalPostCount} = await postService.getAllPosts(res.locals.pagination);
res.locals.posts = posts;
res.locals.totalPostCount = totalPostCount;
res.render('admin/post/index'); res.render('admin/post/index');
} catch (error) { } catch (error) {
this.log.error('failed to fetch posts', { error }); this.log.error('failed to fetch posts', { error });
@ -140,5 +142,6 @@ class PostController extends SiteController {
module.exports = { module.exports = {
name: 'adminPost', name: 'adminPost',
slug: 'admin-post', slug: 'admin-post',
className: 'PostController',
create: async (dtp) => { return new PostController(dtp); }, create: async (dtp) => { return new PostController(dtp); },
}; };

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

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController } = require('../../../lib/site-lib');
class SettingsController extends SiteController { class SettingsAdminController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -127,5 +127,6 @@ class SettingsController extends SiteController {
module.exports = { module.exports = {
name: 'adminSettings', name: 'adminSettings',
slug: 'admin-settings', slug: 'admin-settings',
create: async (dtp) => { return new SettingsController(dtp); }, className: 'SettingsAdminController',
create: async (dtp) => { return new SettingsAdminController(dtp); },
}; };

@ -105,5 +105,6 @@ class SiteLinkAdminController extends SiteController {
module.exports = { module.exports = {
name: 'adminSiteLink', name: 'adminSiteLink',
slug: 'admin-site-link', slug: 'admin-site-link',
className: 'SiteLinkAdminController',
create: async (dtp) => { return new SiteLinkAdminController(dtp); }, create: async (dtp) => { return new SiteLinkAdminController(dtp); },
}; };

@ -6,9 +6,9 @@
const express = require('express'); const express = require('express');
const { SiteController } = require('../../../lib/site-lib'); const { SiteController, SiteError } = require('../../../lib/site-lib');
class UserController extends SiteController { class UserAdminController extends SiteController {
constructor (dtp) { constructor (dtp) {
super(dtp, module.exports); super(dtp, module.exports);
@ -22,37 +22,73 @@ class UserController extends SiteController {
return next(); return next();
}); });
router.param('userId', this.populateUserId.bind(this)); router.param('localUserId', this.populateLocalUserId.bind(this));
router.post('/local/:localUserId', this.postUpdateLocalUser.bind(this));
router.get('/local/:localUserId', this.getLocalUserView.bind(this));
router.post('/:userId', this.postUpdateUser.bind(this));
router.get('/:userId', this.getUserView.bind(this));
router.get('/', this.getHomeView.bind(this)); router.get('/', this.getHomeView.bind(this));
return router; return router;
} }
async populateUserId (req, res, next, userId) { async populateLocalUserId (req, res, next, localUserId) {
const { user: userService } = this.dtp.services; const { user: userService } = this.dtp.services;
try { try {
res.locals.userAccount = await userService.getUserAccount(userId); res.locals.userAccount = await userService.getLocalUserAccount(localUserId);
if (!res.locals.userAccount) {
throw new SiteError(404, 'User not found');
}
return next(); return next();
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
} }
async postUpdateUser (req, res, next) { async postUpdateLocalUser (req, res, next) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
await userService.updateForAdmin(res.locals.userAccount, req.body); this.log.debug('local user update', { action: req.body.action });
switch (req.body.action) {
case 'update':
await userService.updateLocalForAdmin(res.locals.userAccount, req.body);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postUpdateLocalUser',
message: 'local user account updated',
data: {
userAccount: {
_id: res.locals.userAccount._id,
username: res.locals.userAccount.username,
},
},
});
break;
case 'ban':
await userService.ban(res.locals.userAccount);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postUpdateLocalUser',
message: 'local user banned from the app',
data: {
userAccount: {
_id: res.locals.userAccount._id,
username: res.locals.userAccount.username,
},
},
});
break;
}
res.redirect('/admin/user'); res.redirect('/admin/user');
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
} }
async getUserView (req, res, next) { async getLocalUserView (req, res, next) {
const { comment: commentService } = this.dtp.services; const { comment: commentService } = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
@ -68,7 +104,7 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services; const { user: userService } = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 10); res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.userAccounts = await userService.getUserAccounts(res.locals.pagination, req.query.u); res.locals.userAccounts = await userService.searchLocalUserAccounts(res.locals.pagination, req.query.u);
res.locals.totalUserCount = await userService.getTotalCount(); res.locals.totalUserCount = await userService.getTotalCount();
res.render('admin/user/index'); res.render('admin/user/index');
} catch (error) { } catch (error) {
@ -80,5 +116,6 @@ class UserController extends SiteController {
module.exports = { module.exports = {
name: 'adminUser', name: 'adminUser',
slug: 'admin-user', slug: 'admin-user',
create: async (dtp) => { return new UserController(dtp); }, className: 'UserAdminController',
create: async (dtp) => { return new UserAdminController(dtp); },
}; };

@ -138,5 +138,6 @@ class VenueAdminController extends SiteController {
module.exports = { module.exports = {
name: 'adminVenue', name: 'adminVenue',
slug: 'admin-venue', slug: 'admin-venue',
className: 'VenueAdminController',
create: async (dtp) => { return new VenueAdminController(dtp); }, create: async (dtp) => { return new VenueAdminController(dtp); },
}; };

@ -78,8 +78,6 @@ class AnnouncementController extends SiteController {
module.exports = { module.exports = {
slug: 'announcement', slug: 'announcement',
name: 'announcement', name: 'announcement',
create: async (dtp) => { className: 'AnnouncementController',
let controller = new AnnouncementController(dtp); create: async (dtp) => { return new AnnouncementController(dtp); },
return controller;
},
}; };

@ -84,7 +84,10 @@ class AuthController extends SiteController {
} }
async postOtpEnable (req, res, next) { async postOtpEnable (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services; const {
logan: loganService,
otpAuth: otpAuthService,
} = this.dtp.services;
const service = req.body['otp-service']; const service = req.body['otp-service'];
const secret = req.body['otp-secret']; const secret = req.body['otp-secret'];
@ -95,17 +98,33 @@ class AuthController extends SiteController {
this.log.info('enabling OTP protections', { service, secret, token }); this.log.info('enabling OTP protections', { service, secret, token });
res.locals.otpAccount = await otpAuthService.createOtpAccount(req, service, secret, token); res.locals.otpAccount = await otpAuthService.createOtpAccount(req, service, secret, token);
res.locals.otpRedirectURL = otpRedirectURL; res.locals.otpRedirectURL = otpRedirectURL;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postOtpEnable',
data: {
user: {
_id: req.user._id,
username: req.user.username,
},
},
});
res.render('otp/new-account'); res.render('otp/new-account');
} catch (error) { } catch (error) {
this.log.error('failed to enable OTP protections', { loganService.sendRequestEvent(module.exports, req, {
service, error, level: 'error',
event: 'postOtpEnable',
message: `failed to enable OTP account: ${error.message}`,
data: { service, error },
}); });
return next(error); return next(error);
} }
} }
async postOtpAuthenticate (req, res, next) { async postOtpAuthenticate (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services; const {
logan: loganService,
otpAuth: otpAuthService,
} = this.dtp.services;
if (!req.user) { if (!req.user) {
return res.status(403).json({ return res.status(403).json({
@ -129,36 +148,87 @@ class AuthController extends SiteController {
} }
try { try {
await otpAuthService.startOtpSession(req, service, passcode); await otpAuthService.startOtpSession(req, service, passcode);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postOtpAuthenticate',
data: {
user: {
_id: req.user._id,
username: req.user.username,
},
},
});
return res.redirect(req.body['otp-redirect']); return res.redirect(req.body['otp-redirect']);
} catch (error) { } catch (error) {
this.log.error('failed to verify one-time password for 2FA', { loganService.sendRequestEvent(module.exports, req, {
service, error, level: 'error',
}, req.user); event: 'postOtpAuthenticate',
message: `failed to verify one-time password: ${error.message}`,
data: {
user: {
_id: req.user._id,
username: req.user.username,
},
error,
},
});
return next(error); return next(error);
} }
} }
async postLogin (req, res, next) { async postLogin (req, res, next) {
const { logan: loganService } = this.dtp.services;
const redirectUri = req.session.loginReturnTo || '/'; const redirectUri = req.session.loginReturnTo || '/';
this.log.debug('starting passport.authenticate', { redirectUri }); this.log.debug('starting passport.authenticate', { redirectUri });
passport.authenticate('dtp-local', (error, user/*, info*/) => { passport.authenticate('dtp-local', (error, user/*, info*/) => {
if (error) { if (error) {
req.session.loginResult = error.toString(); req.session.loginResult = error.toString();
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postLogin',
message: `login failed: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
if (!user) { if (!user) {
req.session.loginResult = 'Username or email address is unknown.'; req.session.loginResult = 'Username or email address is unknown.';
loganService.sendRequestEvent(module.exports, req, {
level: 'alert',
event: 'postLogin',
message: 'username or email address is unknown',
});
return res.redirect('/welcome/login'); return res.redirect('/welcome/login');
} }
this.log.info('user logging in', { user: user.username }); this.log.info('user logging in', { user: user.username });
req.login(user, async (error) => { req.login(user, async (error) => {
if (error) { if (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postLogin',
message: `failed to start user session: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
// scrub login return URL from session // scrub login return URL from session
delete req.session.loginReturnTo; delete req.session.loginReturnTo;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postLogin',
message: 'session started for site member',
data: {
user: {
_id: user._id,
username: user.username,
},
},
});
// redirect to whatever was wanted // redirect to whatever was wanted
return res.redirect(redirectUri); return res.redirect(redirectUri);
}); });
@ -166,20 +236,29 @@ class AuthController extends SiteController {
} }
async getPersonalApiToken (req, res, next) { async getPersonalApiToken (req, res, next) {
const {
apiGuard: apiGuardService,
logan: loganService,
} = this.dtp.platform.services;
try { try {
const { apiGuard: apiGuardService } = this.dtp.platform.services;
res.locals.apiToken = await apiGuardService.createApiToken(req.user, [ res.locals.apiToken = await apiGuardService.createApiToken(req.user, [
'account-read', 'account-read',
// additional scopes go here // additional scopes go here
]); ]);
res.render('api-token/view'); res.render('api-token/view');
} catch (error) { } catch (error) {
this.log.error('failed to generate API token', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getPersonalApiToken',
message: `failed to generate API token: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getSocketToken (req, res, next) { async getSocketToken (req, res, next) {
const { logan: loganService } = this.dtp.services;
try { try {
const token = await ConnectToken.create({ const token = await ConnectToken.create({
created: new Date(), created: new Date(),
@ -192,35 +271,68 @@ class AuthController extends SiteController {
token: token.token token: token.token
}); });
} catch (error) { } catch (error) {
this.log.error('failed to create Socket.io connect token', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getSocketToken',
message: `failed to create socket token: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getCoreHome (req, res, next) { async getCoreHome (req, res, next) {
const { coreNode: coreNodeService } = this.dtp.services; const {
coreNode: coreNodeService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.currentView = 'welcome'; res.locals.currentView = 'welcome';
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.connectedCores = await coreNodeService.getConnectedCores(res.locals.pagination); res.locals.connectedCores = await coreNodeService.getConnectedCores(res.locals.pagination);
res.render('welcome/core-home'); res.render('welcome/core-home');
} catch (error) { } catch (error) {
this.log.error('failed to generate Core auth menu', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getCoreHome',
message: `failed to render view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getLogout (req, res, next) { async getLogout (req, res, next) {
const { logan: loganService } = this.dtp.services;
if (!req.user) { if (!req.user) {
return next(new SiteError(403, 'You are not signed in')); return next(new SiteError(403, 'You are not signed in'));
} }
const user = {
_id: req.user._id,
username: req.user.username,
};
req.logout(); req.logout();
req.session.destroy((err) => { req.session.destroy((err) => {
if (err) { if (err) {
this.log.error('failed to destroy browser session', { err }); this.log.error('failed to destroy browser session', { err });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getLogout',
message: 'failed to destroy browser session',
data: { error: err },
});
return next(err); return next(err);
} }
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getLogout',
message: 'user terminated their browser session',
data: { user },
});
res.redirect('/'); res.redirect('/');
}); });
} }
@ -229,5 +341,6 @@ class AuthController extends SiteController {
module.exports = { module.exports = {
slug: 'auth', slug: 'auth',
name: 'auth', name: 'auth',
className: 'AuthController',
create: async (dtp) => { return new AuthController(dtp); }, create: async (dtp) => { return new AuthController(dtp); },
}; };

@ -26,15 +26,11 @@ class AuthorController extends SiteController {
return res.redirect(302, '/welcome'); return res.redirect(302, '/welcome');
} }
if (req.user.flags.isAdmin) {
return next();
}
const canAuthor = req.user.permissions.canAuthorPosts; const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts; const canPublish = req.user.permissions.canPublishPosts;
if (!canAuthor && !canPublish) { if (!canAuthor && !canPublish) {
return next(new SiteError(403, 'Author privileges are required')); return next(new SiteError(403, 'Author or Publisher privileges are required'));
} }
return next(); return next();
} }
@ -69,29 +65,35 @@ class AuthorController extends SiteController {
async getPublishedPostHome (req, res, next) { async getPublishedPostHome (req, res, next) {
const { post: postService } = this.dtp.services; const { post: postService } = this.dtp.services;
try { try {
const isAdmin = req.user.flags.isAdmin;
const canAuthor = req.user.permissions.canAuthorPosts; const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts; const canPublish = req.user.permissions.canPublishPosts;
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.published = { };
if(canAuthor) { if(canAuthor) {
if ( canPublish ) { if ( canPublish ) {
res.locals.published = await postService.getPosts( { skip: 0, cpp: 5 }, ['published'], true ); const {posts, totalPostCount }= await postService.getPosts( res.locals.pagination, ['published'], true );
res.locals.published.posts = posts;
res.locals.published.posts.totalPostCount = totalPostCount;
res.locals.allPosts = true; res.locals.allPosts = true;
res.locals.published.all = true; res.locals.published.posts.all = true;
} else { } else {
res.locals.published = await postService.getForAuthor( req.user, ['published'], { skip: 0, cpp: 5 } ); res.locals.published = await postService.getForAuthor( req.user, ['published'], res.locals.pagination );
} }
} }
else if ( canPublish || isAdmin ) { else if ( canPublish) {
res.locals.published = await postService.getPosts( { skip: 0, cpp: 5 }, ['published'], true ); const {posts, totalPostCount }= await postService.getPosts( res.locals.pagination, ['published'], true );
res.locals.published.posts = posts;
res.locals.published.posts.totalPostCount = totalPostCount;
res.locals.published.all = true; res.locals.published.all = true;
} }
@ -105,31 +107,37 @@ class AuthorController extends SiteController {
async getDraftsHome (req, res, next) { async getDraftsHome (req, res, next) {
const { post: postService } = this.dtp.services; const { post: postService } = this.dtp.services;
try { try {
const isAdmin = req.user.flags.isAdmin;
const canAuthor = req.user.permissions.canAuthorPosts; const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts; const canPublish = req.user.permissions.canPublishPosts;
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
const status = ['draft']; const status = ['draft'];
res.locals.drafts = { };
if(canAuthor) { if(canAuthor) {
if ( canPublish ) { if ( canPublish ) {
const {posts, totalPostCount }= await postService.getPosts( res.locals.pagination, status, true );
res.locals.drafts = await postService.getPosts( { skip: 0, cpp: 5 }, status, true ); res.locals.drafts.posts = posts;
res.locals.drafts.posts.totalPostCount = totalPostCount;
res.locals.allPosts = true; res.locals.allPosts = true;
res.locals.drafts.all = true; res.locals.drafts.posts.all = true;
} else { } else {
res.locals.drafts = await postService.getForAuthor( req.user, status, { skip: 0, cpp: 5 } ); res.locals.drafts = await postService.getForAuthor( req.user, status, res.locals.pagination );
} }
} }
else if ( canPublish || isAdmin ) { else if ( canPublish) {
res.locals.drafts = await postService.getPosts( { skip: 0, cpp: 5 }, status, true ); const {posts, totalPostCount }= await postService.getPosts( res.locals.pagination, status, true );
res.locals.drafts.all = true;
res.locals.drafts.posts = posts;
res.locals.drafts.posts.totalPostCount = totalPostCount;
res.locals.allPosts = true;
res.locals.drafts.posts.all = true;
} }
res.render('author/draft/index'); res.render('author/draft/index');

@ -132,7 +132,10 @@ class ChatController extends SiteController {
} }
async populateRoomId (req, res, next, roomId) { async populateRoomId (req, res, next, roomId) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.room = await chatService.getRoomById(roomId); res.locals.room = await chatService.getRoomById(roomId);
if (!res.locals.room) { if (!res.locals.room) {
@ -141,12 +144,21 @@ class ChatController extends SiteController {
return next(); return next();
} catch (error) { } catch (error) {
this.log.error('failed to populate roomId', { roomId, error }); this.log.error('failed to populate roomId', { roomId, error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateRoomId',
message: error.message,
data: { roomId, error },
});
return next(error); return next(error);
} }
} }
async populateInviteId (req, res, next, inviteId) { async populateInviteId (req, res, next, inviteId) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.invite = await chatService.getRoomInviteById(inviteId); res.locals.invite = await chatService.getRoomInviteById(inviteId);
if (!res.locals.invite) { if (!res.locals.invite) {
@ -155,12 +167,21 @@ class ChatController extends SiteController {
return next(); return next();
} catch (error) { } catch (error) {
this.log.error('failed to populate inviteId', { inviteId, error }); this.log.error('failed to populate inviteId', { inviteId, error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateInviteId',
message: error.message,
data: { inviteId, error },
});
return next(error); return next(error);
} }
} }
async postRoomInviteAction (req, res) { async postRoomInviteAction (req, res) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
const { response } = req.body; const { response } = req.body;
const displayList = this.createDisplayList('room-invite-action'); const displayList = this.createDisplayList('room-invite-action');
@ -168,11 +189,27 @@ class ChatController extends SiteController {
switch (response) { switch (response) {
case 'accept': case 'accept':
await chatService.acceptRoomInvite(res.locals.invite); await chatService.acceptRoomInvite(res.locals.invite);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postRoomInviteAction',
message: 'invitation accepted successfully',
data: {
invite: res.locals.invite,
},
});
displayList.navigateTo(`/chat/room/${res.locals.invite.room._id}`); displayList.navigateTo(`/chat/room/${res.locals.invite.room._id}`);
break; break;
case 'reject': case 'reject':
await chatService.rejectRoomInvite(res.locals.invite); await chatService.rejectRoomInvite(res.locals.invite);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postRoomInviteAction',
message: 'invitation rejected successfully',
data: {
invite: res.locals.invite,
},
});
displayList.showNotification( displayList.showNotification(
`Chat room invite rejected`, `Chat room invite rejected`,
'success', 'success',
@ -192,6 +229,12 @@ class ChatController extends SiteController {
response: req.body.response, response: req.body.response,
error, error,
}); });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postRoomInviteAction',
message: `failed to execute room invite action: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -200,7 +243,11 @@ class ChatController extends SiteController {
} }
async postRoomInvite (req, res) { async postRoomInvite (req, res) {
const { chat: chatService, user: userService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
user: userService,
} = this.dtp.services;
this.log.debug('room invite received', { invite: req.body }); this.log.debug('room invite received', { invite: req.body });
if (!req.body.username || !req.body.username.length) { if (!req.body.username || !req.body.username.length) {
return res.status(400).json({ success: false, message: 'Please provide a username' }); return res.status(400).json({ success: false, message: 'Please provide a username' });
@ -223,7 +270,7 @@ class ChatController extends SiteController {
throw new SiteError(400, "You can't invite yourself."); throw new SiteError(400, "You can't invite yourself.");
} }
await chatService.sendRoomInvite(res.locals.room, member, req.body); res.locals.invite = await chatService.sendRoomInvite(res.locals.room, member, req.body);
const displayList = this.createDisplayList('invite create'); const displayList = this.createDisplayList('invite create');
displayList.showNotification( displayList.showNotification(
@ -232,9 +279,28 @@ class ChatController extends SiteController {
'top-left', 'top-left',
5000, 5000,
); );
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postRoomInvite',
data: {
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
invite: res.locals.invite,
},
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to create room invitation', { error }); this.log.error('failed to create room invitation', { error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postRoomInvite',
message: `failed to create room invitation: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -243,35 +309,78 @@ class ChatController extends SiteController {
} }
async postRoomUpdate (req, res, next) { async postRoomUpdate (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.room = await chatService.updateRoom(res.locals.room, req.body); res.locals.room = await chatService.updateRoom(res.locals.room, req.body);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postRoomUpdate',
data: {
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
},
});
res.redirect(`/chat/room/${res.locals.room._id}`); res.redirect(`/chat/room/${res.locals.room._id}`);
} catch (error) { } catch (error) {
this.log.error('failed to update chat room', { loganService.sendRequestEvent(module.exports, req, {
// roomId: res.locals.room._id, level: 'error',
error, event: 'postRoomUpdate',
message: `failed to update chat room: ${error.message}`,
data: { error },
}); });
return next(error); return next(error);
} }
} }
async postRoomCreate (req, res, next) { async postRoomCreate (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.room = await chatService.createRoom(req.user, req.body); res.locals.room = await chatService.createRoom(req.user, req.body);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postRoomCreate',
message: 'chat room created',
data: {
room: res.locals.room,
},
});
res.redirect(`/chat/room/${res.locals.room._id}`); res.redirect(`/chat/room/${res.locals.room._id}`);
} catch (error) { } catch (error) {
this.log.error('failed to create chat room', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postRoomCreate',
message: `failed to create chat room: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getRoomEditor (req, res) { async getRoomEditor (req, res) {
const { logan: loganService } = this.dtp.services;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomEditor',
data: {
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
},
});
res.render('chat/room/editor'); res.render('chat/room/editor');
} }
async getRoomForm (req, res, next) { async getRoomForm (req, res, next) {
const { logan: loganService } = this.dtp.services;
const validFormNames = [ const validFormNames = [
'invite-member', 'invite-member',
]; ];
@ -280,65 +389,148 @@ class ChatController extends SiteController {
return next(new SiteError(404, 'Form not found')); return next(new SiteError(404, 'Form not found'));
} }
try { try {
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomForm',
data: { formName },
});
res.render(`chat/room/form/${formName}`); res.render(`chat/room/form/${formName}`);
} catch (error) { } catch (error) {
this.log.error('failed to render form', { formName, error }); this.log.error('failed to render form', { formName, error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getRoomForm',
message: `failed to render form: ${error.message}`,
data: { formName, error },
});
return next(error); return next(error);
} }
} }
async getRoomInviteView (req, res) { async getRoomInviteView (req, res) {
const { logan: loganService } = this.dtp.services;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomInviteView',
data: {
invite: res.locals.invite,
},
});
res.render('chat/room/invite/view'); res.render('chat/room/invite/view');
} }
async getRoomInviteHome (req, res, next) { async getRoomInviteHome (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.invites = { res.locals.invites = {
new: await chatService.getRoomInvites(res.locals.room, 'new'), new: await chatService.getRoomInvites(res.locals.room, 'new'),
accepted: await chatService.getRoomInvites(res.locals.room, 'accepted'), accepted: await chatService.getRoomInvites(res.locals.room, 'accepted'),
rejected: await chatService.getRoomInvites(res.locals.room, 'rejected'), rejected: await chatService.getRoomInvites(res.locals.room, 'rejected'),
}; };
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomInviteHome',
});
res.render('chat/room/invite'); res.render('chat/room/invite');
} catch (error) { } catch (error) {
this.log.error('failed to render the room invites view', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getRoomInviteHome',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getRoomSettings (req, res) { async getRoomSettings (req, res) {
const { logan: loganService } = this.dtp.services;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomSettings',
data: {
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
},
});
res.render('chat/room/editor'); res.render('chat/room/editor');
} }
async getRoomView (req, res, next) { async getRoomView (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.pageTitle = res.locals.room.name; res.locals.pageTitle = res.locals.room.name;
const pagination = { skip: 0, cpp: 20 }; const pagination = { skip: 0, cpp: 20 };
res.locals.chatMessages = await chatService.getChannelHistory(res.locals.room, pagination); res.locals.chatMessages = await chatService.getChannelHistory(res.locals.room, pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomView',
data: {
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
},
});
res.render('chat/room/view'); res.render('chat/room/view');
} catch (error) { } catch (error) {
this.log.error('failed to render chat room view', { roomId: req.params.roomId, error }); this.log.error('failed to render chat room view', { roomId: req.params.roomId, error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getRoomView',
message: `failed to render the view: ${error.message}`,
data: {
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
error,
},
});
return next(error); return next(error);
} }
} }
async getRoomHome (req, res, next) { async getRoomHome (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.publicRooms = await chatService.getPublicRooms(req.user, res.locals.pagination); res.locals.publicRooms = await chatService.getPublicRooms(req.user, res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomHome',
});
res.render('chat/room/index'); res.render('chat/room/index');
} catch (error) { } catch (error) {
this.log.error('failed to render room home', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getRoomHome',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getHome (req, res, next) { async getHome (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.pageTitle = 'Chat Home'; res.locals.pageTitle = 'Chat Home';
@ -349,15 +541,28 @@ class ChatController extends SiteController {
res.locals.joinedChatRooms.forEach((room) => roomIds.push(room._id)); res.locals.joinedChatRooms.forEach((room) => roomIds.push(room._id));
res.locals.timeline = await chatService.getMultiRoomTimeline(roomIds, res.locals.pagination); res.locals.timeline = await chatService.getMultiRoomTimeline(roomIds, res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHome',
});
res.render('chat/index'); res.render('chat/index');
} catch (error) { } catch (error) {
this.log.error('failed to render chat home', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getHome',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async deleteInvite (req, res, next) { async deleteInvite (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
if (res.locals.room.owner._id.equals(req.user._id)) { if (res.locals.room.owner._id.equals(req.user._id)) {
throw new SiteError(403, 'This is not your invitiation'); throw new SiteError(403, 'This is not your invitiation');
@ -374,15 +579,34 @@ class ChatController extends SiteController {
5000, 5000,
); );
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'deleteInvite',
message: 'room invitation deleted',
data: {
invite: {
_id: res.locals.invite._id,
},
},
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to delete chat room invite', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'deleteInvite',
message: `failed to delete chat room invite: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async deleteRoom (req, res, next) { async deleteRoom (req, res, next) {
const { chat: chatService } = this.dtp.services; const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try { try {
if (res.locals.room.owner._id.equals(req.user._id)) { if (res.locals.room.owner._id.equals(req.user._id)) {
throw new SiteError(403, 'This is not your chat room'); throw new SiteError(403, 'This is not your chat room');
@ -393,9 +617,26 @@ class ChatController extends SiteController {
const displayList = this.createDisplayList('delete chat invite'); const displayList = this.createDisplayList('delete chat invite');
displayList.navigateTo('/chat'); displayList.navigateTo('/chat');
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'deleteRoom',
message: 'chat room deleted',
data: {
room: {
_id: res.locals.room._id,
name: res.locals.room.name,
},
},
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to delete chat room invite', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'deleteRoom',
message: `failed to delete chat room: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
@ -404,5 +645,6 @@ class ChatController extends SiteController {
module.exports = { module.exports = {
slug: 'chat', slug: 'chat',
name: 'chat', name: 'chat',
className: 'ChatController',
create: async (dtp) => { return new ChatController(dtp); }, create: async (dtp) => { return new ChatController(dtp); },
}; };

@ -153,5 +153,6 @@ class CommentController extends SiteController {
module.exports = { module.exports = {
slug: 'comment', slug: 'comment',
name: 'comment', name: 'comment',
className: 'CommentController',
create: async (dtp) => { return new CommentController(dtp); }, create: async (dtp) => { return new CommentController(dtp); },
}; };

@ -38,27 +38,43 @@ class EmailController extends SiteController {
} }
async getEmailOptOut (req, res, next) { async getEmailOptOut (req, res, next) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
await userService.emailOptOut(req.query.u, req.query.c); await userService.emailOptOut(req.query.u, req.query.c);
res.render('email/opt-out-success'); res.render('email/opt-out-success');
} catch (error) { } catch (error) {
this.log.error('failed to opt-out from email', { loganService.sendRequestEvent(module.exports, req, {
userId: req.query.t, level: 'error',
event: 'getEmailOptOut',
message: 'failed to opt-out from email',
data: {
userId: req.query.u,
category: req.query.c, category: req.query.c,
error, error,
},
}); });
return next(error); return next(error);
} }
} }
async getEmailVerify (req, res, next) { async getEmailVerify (req, res, next) {
const { email: emailService } = this.dtp.services; const {
email: emailService,
logan: loganService,
} = this.dtp.services;
try { try {
await emailService.verifyToken(req.query.t); await emailService.verifyToken(req.query.t);
res.render('email/verify-success'); res.render('email/verify-success');
} catch (error) { } catch (error) {
this.log.error('failed to verify email', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getEmailVerify',
message: `failed to verify email: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
@ -67,5 +83,6 @@ class EmailController extends SiteController {
module.exports = { module.exports = {
slug: 'email', slug: 'email',
name: 'email', name: 'email',
className: 'EmailController',
create: async (dtp) => { return new EmailController(dtp); }, create: async (dtp) => { return new EmailController(dtp); },
}; };

@ -66,5 +66,6 @@ class FormController extends SiteController {
module.exports = { module.exports = {
slug: 'form', slug: 'form',
name: 'form', name: 'form',
className: 'FormController',
create: async (dtp) => { return new FormController(dtp); }, create: async (dtp) => { return new FormController(dtp); },
}; };

@ -59,5 +59,6 @@ class HiveController extends SiteController {
module.exports = { module.exports = {
slug: 'hive', slug: 'hive',
name: 'hive', name: 'hive',
className: 'HiveController',
create: async (dtp) => { return new HiveController(dtp); }, create: async (dtp) => { return new HiveController(dtp); },
}; };

@ -95,5 +95,6 @@ class HiveKaleidoscopeController extends SiteController {
module.exports = { module.exports = {
name: 'hiveKaleidoscope', name: 'hiveKaleidoscope',
slug: 'hive-kaleidoscope', slug: 'hive-kaleidoscope',
className: 'HiveKaleidoscopeController',
create: async (dtp) => { return new HiveKaleidoscopeController(dtp); }, create: async (dtp) => { return new HiveKaleidoscopeController(dtp); },
}; };

@ -78,7 +78,7 @@ class HiveUserController extends SiteController {
throw new SiteError(406, 'Must include search term'); throw new SiteError(406, 'Must include search term');
} }
res.locals.q = await userService.filterUsername(req.query.q); res.locals.q = userService.filterUsername(req.query.q);
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.userProfiles = await userService.getUserAccounts(res.locals.pagination, res.locals.q); res.locals.userProfiles = await userService.getUserAccounts(res.locals.pagination, res.locals.q);
res.locals.userProfiles = res.locals.userProfiles.map((user) => { res.locals.userProfiles = res.locals.userProfiles.map((user) => {
@ -151,8 +151,6 @@ class HiveUserController extends SiteController {
module.exports = { module.exports = {
name: 'hiveUser', name: 'hiveUser',
slug: 'hive-user', slug: 'hive-user',
create: async (dtp) => { className: 'HiveUserController',
let controller = new HiveUserController(dtp); create: async (dtp) => { return new HiveUserController(dtp); },
return controller;
},
}; };

@ -63,7 +63,12 @@ class HomeController extends SiteController {
} }
async getHome (req, res, next) { async getHome (req, res, next) {
const { announcement: announcementService, hive: hiveService, post: postService } = this.dtp.services; const {
announcement: announcementService,
hive: hiveService,
logan: loganService,
post: postService,
} = this.dtp.services;
try { try {
res.locals.announcements = await announcementService.getLatest(req.user); res.locals.announcements = await announcementService.getLatest(req.user);
res.locals.featuredPosts = await postService.getFeaturedPosts(3); res.locals.featuredPosts = await postService.getFeaturedPosts(3);
@ -72,17 +77,28 @@ class HomeController extends SiteController {
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination); res.locals.posts = await postService.getPosts(res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHome',
});
res.render('index'); res.render('index');
} catch (error) { } catch (error) {
this.log.error('failed to render home view', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getHome',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
} }
module.exports = { module.exports = {
isHome: true,
slug: 'home', slug: 'home',
name: 'home', name: 'home',
isHome: true, className: 'HomeController',
create: async (dtp) => { return new HomeController(dtp); }, create: async (dtp) => { return new HomeController(dtp); },
}; };

@ -134,5 +134,6 @@ class ImageController extends SiteController {
module.exports = { module.exports = {
slug: 'image', slug: 'image',
name: 'image', name: 'image',
className: 'ImageController',
create: async (dtp) => { return new ImageController(dtp); }, create: async (dtp) => { return new ImageController(dtp); },
}; };

@ -66,5 +66,6 @@ class ManifestController extends SiteController {
module.exports = { module.exports = {
slug: 'manifest', slug: 'manifest',
name: 'manifest', name: 'manifest',
className: 'ManifestController',
create: async (dtp) => { return new ManifestController(dtp); }, create: async (dtp) => { return new ManifestController(dtp); },
}; };

@ -94,8 +94,6 @@ class NewsletterController extends SiteController {
module.exports = { module.exports = {
slug: 'newsletter', slug: 'newsletter',
name: 'newsletter', name: 'newsletter',
create: async (dtp) => { className: 'NewsletterController',
let controller = new NewsletterController(dtp); create: async (dtp) => { return new NewsletterController(dtp); },
return controller;
},
}; };

@ -81,5 +81,6 @@ class NewsroomController extends SiteController {
module.exports = { module.exports = {
slug: 'newsroom', slug: 'newsroom',
name: 'newsroom', name: 'newsroom',
className: 'NewsroomController',
create: (dtp) => { return new NewsroomController(dtp); }, create: (dtp) => { return new NewsroomController(dtp); },
}; };

@ -74,5 +74,6 @@ class NotificationController extends SiteController {
module.exports = { module.exports = {
slug: 'notification', slug: 'notification',
name: 'notification', name: 'notification',
className: 'NotificationController',
create: async (dtp) => { return new NotificationController(dtp); }, create: async (dtp) => { return new NotificationController(dtp); },
}; };

@ -65,8 +65,6 @@ class PageController extends SiteController {
module.exports = { module.exports = {
slug: 'page', slug: 'page',
name: 'page', name: 'page',
create: async (dtp) => { className: 'PageController',
let controller = new PageController(dtp); create: async (dtp) => { return new PageController(dtp); },
return controller;
},
}; };

@ -33,8 +33,8 @@ class PostController extends SiteController {
if (req.user && req.user.flags.isAdmin) { if (req.user && req.user.flags.isAdmin) {
return next(); return next();
} }
if (!req.user || !req.flags.isAdmin) { if (!req.user || !req.user.permissions.canAuthorPages) {
return next(new SiteError(403, 'Author or admin privileges are required')); return next(new SiteError(403, 'Author privileges are required'));
} }
return next(); return next();
} }
@ -47,6 +47,7 @@ class PostController extends SiteController {
router.param('username', this.populateUsername.bind(this)); router.param('username', this.populateUsername.bind(this));
router.param('postSlug', this.populatePostSlug.bind(this)); router.param('postSlug', this.populatePostSlug.bind(this));
router.param('postId', this.populatePostId.bind(this)); router.param('postId', this.populatePostId.bind(this));
router.param('tagSlug', this.populateTagSlug.bind(this));
router.param('commentId', commentService.populateCommentId.bind(commentService)); router.param('commentId', commentService.populateCommentId.bind(commentService));
@ -78,6 +79,8 @@ class PostController extends SiteController {
this.getAllAuthorsView.bind(this), this.getAllAuthorsView.bind(this),
); );
router.get('/tags', this.getTagIndex.bind(this));
router.get('/:postSlug', router.get('/:postSlug',
limiterService.createMiddleware(limiterService.config.post.getView), limiterService.createMiddleware(limiterService.config.post.getView),
this.getView.bind(this), this.getView.bind(this),
@ -101,7 +104,9 @@ class PostController extends SiteController {
this.deletePost.bind(this), this.deletePost.bind(this),
); );
router.get('/*', this.badRoute.bind(this) );
router.get('/tag/:tagSlug', this.getTagSearchView.bind(this));
} }
async populateUsername (req, res, next, username) { async populateUsername (req, res, next, username) {
@ -319,7 +324,7 @@ class PostController extends SiteController {
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to fetch more commnets', { postId: res.locals.post._id, error }); this.log.error('failed to fetch more comments', { postId: res.locals.post._id, error });
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -338,9 +343,9 @@ class PostController extends SiteController {
throw new SiteError(403, 'The post is not published'); throw new SiteError(403, 'The post is not published');
} }
} }
if (res.locals.post.status === 'published') {
await resourceService.recordView(req, 'Post', res.locals.post._id); await resourceService.recordView(req, 'Post', res.locals.post._id);
}
res.locals.countPerPage = 20; res.locals.countPerPage = 20;
res.locals.pagination = this.getPaginationParameters(req, res.locals.countPerPage); res.locals.pagination = this.getPaginationParameters(req, res.locals.countPerPage);
@ -442,13 +447,61 @@ 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) {
try {
res.locals.pageTitle = `Tag ${res.locals.tag} on ${this.dtp.config.site.name}`;
res.render('post/tag/view');
} catch (error) {
this.log.error('failed to service post view', { postId: res.locals.post._id, error });
throw SiteError("Error getting tag view:", error );
}
}
async getTagIndex (req, res, next) {
const { post: postService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination);
res.render('post/tag/index');
} catch (error) {
return next(error);
}
}
} }
module.exports = { module.exports = {
slug: 'post', slug: 'post',
name: 'post', name: 'post',
create: async (dtp) => { className: 'PostController',
let controller = new PostController(dtp); create: async (dtp) => { return new PostController(dtp); },
return controller;
},
}; };

@ -18,12 +18,17 @@ class UserController extends SiteController {
async start ( ) { async start ( ) {
const { dtp } = this; const { dtp } = this;
const { const {
csrfToken: csrfTokenService,
limiter: limiterService, limiter: limiterService,
otpAuth: otpAuthService, otpAuth: otpAuthService,
session: sessionService, session: sessionService,
} = dtp.services; } = dtp.services;
const upload = this.createMulter(); const upload = this.createMulter('user', {
limits: {
fileSize: 1024 * 1000 * 5, // 5MB
},
});
const router = express.Router(); const router = express.Router();
dtp.app.use('/user', router); dtp.app.use('/user', router);
@ -60,8 +65,10 @@ class UserController extends SiteController {
return next(); return next();
} }
router.param('username', this.populateUsername.bind(this)); router.param('localUsername', this.populateLocalUsername.bind(this));
router.param('userId', this.populateUserId.bind(this)); router.param('coreUsername', this.populateCoreUsername.bind(this));
router.param('localUserId', this.populateLocalUserId.bind(this));
router.param('coreUserId', this.populateCoreUserId.bind(this)); router.param('coreUserId', this.populateCoreUserId.bind(this));
router.post( router.post(
@ -73,7 +80,7 @@ class UserController extends SiteController {
); );
router.post( router.post(
'/:userId/profile-photo', '/:localUserId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.postProfilePhoto), limiterService.createMiddleware(limiterService.config.user.postProfilePhoto),
checkProfileOwner, checkProfileOwner,
upload.single('imageFile'), upload.single('imageFile'),
@ -81,7 +88,7 @@ class UserController extends SiteController {
); );
router.post( router.post(
'/:userId/settings', '/:localUserId/settings',
limiterService.createMiddleware(limiterService.config.user.postUpdateSettings), limiterService.createMiddleware(limiterService.config.user.postUpdateSettings),
checkProfileOwner, checkProfileOwner,
upload.none(), upload.none(),
@ -91,6 +98,7 @@ class UserController extends SiteController {
router.post( router.post(
'/', '/',
limiterService.createMiddleware(limiterService.config.user.postCreate), limiterService.createMiddleware(limiterService.config.user.postCreate),
csrfTokenService.middleware({ name: 'user-create' }),
this.postCreateUser.bind(this), this.postCreateUser.bind(this),
); );
@ -124,7 +132,7 @@ class UserController extends SiteController {
); );
router.get( router.get(
'/:userId/settings', '/:localUsername/settings',
limiterService.createMiddleware(limiterService.config.user.getSettings), limiterService.createMiddleware(limiterService.config.user.getSettings),
authRequired, authRequired,
otpMiddleware, otpMiddleware,
@ -132,7 +140,7 @@ class UserController extends SiteController {
this.getUserSettingsView.bind(this), this.getUserSettingsView.bind(this),
); );
router.get( router.get(
'/:username', '/:localUsername',
limiterService.createMiddleware(limiterService.config.user.getUserProfile), limiterService.createMiddleware(limiterService.config.user.getUserProfile),
authRequired, authRequired,
otpMiddleware, otpMiddleware,
@ -148,54 +156,127 @@ class UserController extends SiteController {
); );
} }
async populateUsername (req, res, next, username) { async populateCoreUsername (req, res, next, coreUsername) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
res.locals.userProfile = await userService.getPublicProfile('User', username); res.locals.username = userService.filterUsername(coreUsername);
if (!res.locals.userProfile) { res.locals.userProfileId = await userService.getCoreUserId(res.locals.username);
throw new SiteError(404, 'Member not found'); if (!res.locals.userProfileId) {
throw new SiteError(404, 'Core member not found');
} }
return next(); // manually chain over to the ID parameter resolver
return this.populateCoreUserId(req, res, next, res.locals.userProfileId);
} catch (error) { } catch (error) {
this.log.error('failed to populate username with public profile', { username, error }); this.log.error('failed to populate core username', { coreUsername, error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateCoreUsername',
message: `failed to populate Core user: ${error.message}`,
data: { coreUsername, error },
});
return next(error); return next(error);
} }
} }
async populateUserId (req, res, next, userId) { async populateCoreUserId (req, res, next, coreUserId) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
userId = mongoose.Types.ObjectId(userId); res.locals.userProfileId = mongoose.Types.ObjectId(coreUserId);
} catch (error) {
return next(new SiteError(406, 'Invalid User')); if (req.user && (req.user.type === 'CoreUser') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getCoreUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getCoreUserProfile(res.locals.userProfileId);
} }
try {
res.locals.userProfile = await userService.getUserAccount(userId); if (!res.locals.userProfile) {
throw new SiteError(404, 'Core member not found');
}
return next(); return next();
} catch (error) { } catch (error) {
this.log.error('failed to populate userId', { userId, error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateCoreUserId',
message: `failed to populate core user: ${error.message}`,
data: { coreUserId, error },
});
return next(error); return next(error);
} }
} }
async populateCoreUserId (req, res, next, coreUserId) { async populateLocalUsername (req, res, next, username) {
const { coreNode: coreNodeService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
coreUserId = mongoose.Types.ObjectId(coreUserId); res.locals.username = userService.filterUsername(username);
res.locals.userProfileId = await userService.getLocalUserId(res.locals.username);
if (!res.locals.userProfileId) {
throw new SiteError(404, 'Local member not found');
}
if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
}
return next();
} catch (error) { } catch (error) {
return next(new SiteError(406, 'Invalid User')); this.log.error('failed to populate local username', { username, error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateLocalUsername',
message: `failed to populate local user: ${error.message}`,
data: { username, error },
});
return next(error);
} }
}
async populateLocalUserId (req, res, next, userId) {
const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
res.locals.userProfile = await coreNodeService.getUserByLocalId(coreUserId); res.locals.userProfileId = mongoose.Types.ObjectId(userId);
if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
}
if (!res.locals.userProfile) {
throw new SiteError(404, 'Local member not found');
}
return next(); return next();
} catch (error) { } catch (error) {
this.log.error('failed to populate coreUserId', { coreUserId, error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateLocalUserId',
message: `failed to populate local user: ${error.message}`,
data: { userId, error },
});
return next(error); return next(error);
} }
} }
async postCreateUser (req, res, next) { async postCreateUser (req, res, next) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
// verify that the request has submitted a captcha // verify that the request has submitted a captcha
if ((typeof req.body.captcha !== 'string') || req.body.captcha.length === 0) { if ((typeof req.body.captcha !== 'string') || req.body.captcha.length === 0) {
@ -213,11 +294,42 @@ class UserController extends SiteController {
// create the user account // create the user account
res.locals.user = await userService.create(req.body); res.locals.user = await userService.create(req.body);
const form = Object.assign(req.body);
if (form.password) {
delete form.password;
}
if (form.passwordv) {
delete form.passwordv;
}
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postCreateUser',
data: {
form,
user: {
_id: res.locals.user._id,
username: res.locals.user.username,
},
},
});
// log the user in // log the user in
req.login(res.locals.user, (error) => { req.login(res.locals.user, (error) => {
if (error) { if (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postCreateUser',
message: `failed to start user session: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postCreateUser',
message: 'user session started',
});
res.redirect(`/user/${res.locals.user.username}`); res.redirect(`/user/${res.locals.user.username}`);
}); });
} catch (error) { } catch (error) {
@ -227,19 +339,35 @@ class UserController extends SiteController {
} }
async postProfilePhoto (req, res) { async postProfilePhoto (req, res) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('profile-photo');
await userService.updatePhoto(req.user, req.file); await userService.updatePhoto(req.user, req.file);
const displayList = this.createDisplayList('profile-photo');
displayList.showNotification( displayList.showNotification(
'Profile photo updated successfully.', 'Profile photo updated successfully.',
'success', 'success',
'bottom-center', 'bottom-center',
2000, 2000,
); );
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postProfilePhoto',
message: 'profile photo updated',
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to update profile photo', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postProfilePhoto',
message: `failed to update profile photo: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -248,19 +376,35 @@ class UserController extends SiteController {
} }
async postHeaderImage (req, res) { async postHeaderImage (req, res) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('header-image');
await userService.updateHeaderImage(req.user, req.file); await userService.updateHeaderImage(req.user, req.file);
const displayList = this.createDisplayList('header-image');
displayList.showNotification( displayList.showNotification(
'Header image updated successfully.', 'Header image updated successfully.',
'success', 'success',
'bottom-center', 'bottom-center',
2000, 2000,
); );
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postHeaderImage',
message: 'header image updated',
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to update header image', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postHeaderImage',
message: `failed to update header image: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -269,16 +413,30 @@ class UserController extends SiteController {
} }
async postUpdateCoreSettings (req, res) { async postUpdateCoreSettings (req, res) {
const { coreNode: coreNodeService } = this.dtp.services; const {
coreNode: coreNodeService,
logan: loganService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('app-settings');
await coreNodeService.updateUserSettings(req.user, req.body); await coreNodeService.updateUserSettings(req.user, req.body);
const displayList = this.createDisplayList('app-settings');
displayList.reload(); displayList.reload();
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postUpdateCoreSettings',
message: 'CoreUser settings updated',
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to update CoreUser settings', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postUpdateCoreSettings',
message: `failed to update CoreUser settings: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -287,16 +445,30 @@ class UserController extends SiteController {
} }
async postUpdateSettings (req, res) { async postUpdateSettings (req, res) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
await userService.updateSettings(req.user, req.body); await userService.updateSettings(req.user, req.body);
const displayList = this.createDisplayList('app-settings'); const displayList = this.createDisplayList('app-settings');
displayList.reload(); displayList.reload();
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postUpdateSettings',
message: 'account settings updates',
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to update account settings', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postUpdateSettings',
message: `failed to update account settings: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -309,13 +481,26 @@ class UserController extends SiteController {
} }
async getOtpDisable (req, res) { async getOtpDisable (req, res) {
const { otpAuth: otpAuthService } = this.dtp.services; const {
logan: loganService,
otpAuth: otpAuthService,
} = this.dtp.services;
try { try {
await otpAuthService.destroyOtpSession(req, 'Account'); await otpAuthService.destroyOtpSession(req, 'Account');
await otpAuthService.removeForUser(req.user, 'Account'); await otpAuthService.removeForUser(req.user, 'Account');
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getOtpDisable',
message: 'one-time passwords disabled',
});
res.render('user/otp-disabled'); res.render('user/otp-disabled');
} catch (error) { } catch (error) {
this.log.error('failed to disable OTP service for Account', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getOtpDisable',
message: `failed to disable OTP: ${error.message}`,
data: { error },
});
res.status(error.statusCode || 500).json({ res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -324,44 +509,71 @@ class UserController extends SiteController {
} }
async getCoreUserSettingsView (req, res, next) { async getCoreUserSettingsView (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services; const {
logan: loganService,
otpAuth: otpAuthService,
} = this.dtp.services;
try { try {
res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account'); res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account');
res.locals.startTab = req.query.st || 'watch'; res.locals.startTab = req.query.st || 'watch';
res.render('user/settings-core'); res.render('user/settings-core');
} catch (error) { } catch (error) {
this.log.error('failed to render CoreUser settings view', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getCoreUserSettingsView',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getUserSettingsView (req, res, next) { async getUserSettingsView (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services; const {
logan: loganService,
otpAuth: otpAuthService,
} = this.dtp.services;
try { try {
res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account'); res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account');
res.locals.startTab = req.query.st || 'watch'; res.locals.startTab = req.query.st || 'watch';
res.render('user/settings'); res.render('user/settings');
} catch (error) { } catch (error) {
this.log.error('failed to render user settings view', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getUserSettingsView',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async getUserView (req, res, next) { async getUserView (req, res, next) {
const { comment: commentService } = this.dtp.services; const {
comment: commentService,
logan: loganService,
} = this.dtp.services;
try { try {
res.locals.pageTitle = `${res.locals.userProfile.username}'s Profile`; res.locals.pageTitle = `${res.locals.userProfile.username}'s Profile`;
res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.commentHistory = await commentService.getForAuthor(req.user, res.locals.pagination); res.locals.commentHistory = await commentService.getForAuthor(req.user, res.locals.pagination);
res.render('user/profile'); res.render('user/profile');
} catch (error) { } catch (error) {
this.log.error('failed to produce user profile view', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getUserView',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error); return next(error);
} }
} }
async deleteProfilePhoto (req, res) { async deleteProfilePhoto (req, res) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('app-settings'); const displayList = this.createDisplayList('app-settings');
await userService.removePhoto(req.user); await userService.removePhoto(req.user);
@ -371,9 +583,19 @@ class UserController extends SiteController {
'bottom-center', 'bottom-center',
2000, 2000,
); );
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'deleteProfilePhoto',
message: 'profile photo removed',
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to remove profile photo', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'deleteProfilePhoto',
message: `failed to remove profile photo: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -382,7 +604,10 @@ class UserController extends SiteController {
} }
async deleteHeaderImage (req, res) { async deleteHeaderImage (req, res) {
const { user: userService } = this.dtp.services; const {
logan: loganService,
user: userService,
} = this.dtp.services;
try { try {
const displayList = this.createDisplayList('remove-header-image'); const displayList = this.createDisplayList('remove-header-image');
await userService.removeHeaderImage(req.user); await userService.removeHeaderImage(req.user);
@ -392,9 +617,19 @@ class UserController extends SiteController {
'bottom-center', 'bottom-center',
2000, 2000,
); );
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'deleteHeaderImage',
message: 'profile header image removed',
});
res.status(200).json({ success: true, displayList }); res.status(200).json({ success: true, displayList });
} catch (error) { } catch (error) {
this.log.error('failed to remove header image', { error }); loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'deleteHeaderImage',
message: `failed to remove header image: ${error.message}`,
data: { error },
});
return res.status(error.statusCode || 500).json({ return res.status(error.statusCode || 500).json({
success: false, success: false,
message: error.message, message: error.message,
@ -406,5 +641,6 @@ class UserController extends SiteController {
module.exports = { module.exports = {
slug: 'user', slug: 'user',
name: 'user', name: 'user',
className: 'UserController',
create: async (dtp) => { return new UserController(dtp); }, create: async (dtp) => { return new UserController(dtp); },
}; };

@ -77,5 +77,6 @@ class VenueController extends SiteController {
module.exports = { module.exports = {
slug: 'venue', slug: 'venue',
name: 'venue', name: 'venue',
className: 'VenueController',
create: async (dtp) => { return new VenueController(dtp); }, create: async (dtp) => { return new VenueController(dtp); },
}; };

@ -65,23 +65,63 @@ class WelcomeController extends SiteController {
} }
async getSignupView (req, res) { async getSignupView (req, res) {
const {
csrfToken: csrfTokenService,
logan: loganService,
} = this.dtp.services;
req.csrfToken = await csrfTokenService.create(req, {
name: 'user-create',
expiresMinutes: 20,
});
req.session.captcha = req.session.captcha || { }; req.session.captcha = req.session.captcha || { };
req.session.captcha.signup = captcha.randomText(4 + Math.floor(Math.random()*4)); req.session.captcha.signup = captcha.randomText(4 + Math.floor(Math.random()*4));
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getSignupView',
message: 'serving new member signup view',
});
res.render('welcome/signup'); res.render('welcome/signup');
} }
async getLoginView (req, res) { async getLoginView (req, res) {
const { logan: loganService } = this.dtp.services;
res.locals.loginResult = req.session.loginResult; res.locals.loginResult = req.session.loginResult;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getLoginView',
message: 'serving member login view',
});
res.render('welcome/login'); res.render('welcome/login');
} }
async getHomeView (req, res) { async getHomeView (req, res, next) {
const { logan: loganService } = this.dtp.services;
try {
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHomeView',
message: 'serving the Welcome home page',
});
res.render('welcome/index'); res.render('welcome/index');
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'getHomeView',
message: `failed to render the view: ${error.message}`,
data: { error },
});
return next(error);
}
} }
} }
module.exports = { module.exports = {
slug: 'welcome', slug: 'welcome',
name: 'welcome', name: 'welcome',
className: 'WelcomeController',
create: async (dtp) => { return new WelcomeController(dtp); }, create: async (dtp) => { return new WelcomeController(dtp); },
}; };

@ -29,4 +29,6 @@ const AnnouncementSchema = new Schema({
resourceStats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, resourceStats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
}); });
module.exports = mongoose.model('Announcement', AnnouncementSchema); module.exports = (conn) => {
return conn.model('Announcement', AnnouncementSchema);
};

@ -61,4 +61,6 @@ AttachmentSchema.index({
name: 'attachment_item_idx', name: 'attachment_item_idx',
}); });
module.exports = mongoose.model('Attachment', AttachmentSchema); module.exports = (conn) => {
return conn.model('Attachment', AttachmentSchema);
};

@ -22,4 +22,6 @@ const CategorySchema = new Schema({
}, },
}); });
module.exports = mongoose.model('Category', CategorySchema); module.exports = (conn) => {
return conn.model('Category', CategorySchema);
};

@ -28,4 +28,6 @@ const ChatMessageSchema = new Schema({
attachments: { type: [Schema.ObjectId], ref: 'Attachment' }, attachments: { type: [Schema.ObjectId], ref: 'Attachment' },
}); });
module.exports = mongoose.model('ChatMessage', ChatMessageSchema); module.exports = (conn) => {
return conn.model('ChatMessage', ChatMessageSchema);
};

@ -27,4 +27,6 @@ ChatRoomInviteSchema.index({
name: 'chatroom_invite_unique_idx', name: 'chatroom_invite_unique_idx',
}); });
module.exports = mongoose.model('ChatRoomInvite', ChatRoomInviteSchema); module.exports = (conn) => {
return conn.model('ChatRoomInvite', ChatRoomInviteSchema);
};

@ -41,4 +41,6 @@ ChatRoomSchema.index({
name: 'chatroom_public_open_idx', name: 'chatroom_public_open_idx',
}); });
module.exports = mongoose.model('ChatRoom', ChatRoomSchema); module.exports = (conn) => {
return conn.model('ChatRoom', ChatRoomSchema);
};

@ -72,4 +72,6 @@ CommentSchema.index({
name: 'comment_replies', name: 'comment_replies',
}); });
module.exports = mongoose.model('Comment', CommentSchema); module.exports = (conn) => {
return conn.model('Comment', CommentSchema);
};

@ -16,4 +16,6 @@ const ConnectTokenSchema = new Schema({
claimed: { type: Date }, claimed: { type: Date },
}); });
module.exports = mongoose.model('ConnectToken', ConnectTokenSchema); module.exports = (conn) => {
return conn.model('ConnectToken', ConnectTokenSchema);
};

@ -28,4 +28,6 @@ ContentReportSchema.index({
name: 'unique_user_content_report', name: 'unique_user_content_report',
}); });
module.exports = mongoose.model('ContentReport', ContentReportSchema); module.exports = (conn) => {
return conn.model('ContentReport', ContentReportSchema);
};

@ -23,4 +23,6 @@ ContentVoteSchema.index({
name: 'unique_user_content_vote', name: 'unique_user_content_vote',
}); });
module.exports = mongoose.model('ContentVote', ContentVoteSchema); module.exports = (conn) => {
return conn.model('ContentVote', ContentVoteSchema);
};

@ -30,4 +30,6 @@ const CoreNodeConnectSchema = new Schema({
}, },
}); });
module.exports = mongoose.model('CoreNodeConnect', CoreNodeConnectSchema); module.exports = (conn) => {
return conn.model('CoreNodeConnect', CoreNodeConnectSchema);
};

@ -35,4 +35,6 @@ const CoreNodeRequestSchema = new Schema({
}, },
}); });
module.exports = mongoose.model('CoreNodeRequest', CoreNodeRequestSchema); module.exports = (conn) => {
return conn.model('CoreNodeRequest', CoreNodeRequestSchema);
};

@ -45,4 +45,6 @@ CoreNodeSchema.index({
name: 'core_address_idx', name: 'core_address_idx',
}); });
module.exports = mongoose.model('CoreNode', CoreNodeSchema); module.exports = (conn) => {
return conn.model('CoreNode', CoreNodeSchema);
};

@ -52,4 +52,6 @@ CoreUserSchema.index({
name: 'core_username_lc_unique', name: 'core_username_lc_unique',
}); });
module.exports = mongoose.model('CoreUser', CoreUserSchema); module.exports = (conn) => {
return conn.model('CoreUser', CoreUserSchema);
};

@ -17,4 +17,6 @@ const CsrfTokenSchema = new Schema({
ip: { type: String, required: true }, ip: { type: String, required: true },
}); });
module.exports = mongoose.model('CsrfToken', CsrfTokenSchema); module.exports = (conn) => {
return conn.model('CsrfToken', CsrfTokenSchema);
};

@ -31,4 +31,6 @@ EmailBlacklistSchema.index({
}, },
}); });
module.exports = mongoose.model('EmailBlacklist', EmailBlacklistSchema); module.exports = (conn) => {
return conn.model('EmailBlacklist', EmailBlacklistSchema);
};

@ -12,4 +12,6 @@ const EmailBodySchema = new Schema({
body: { type: String, required: true }, body: { type: String, required: true },
}); });
module.exports = mongoose.model('EmailBody', EmailBodySchema); module.exports = (conn) => {
return conn.model('EmailBody', EmailBodySchema);
};

@ -16,4 +16,6 @@ const EmailLogSchema = new Schema({
messageId: { type: String }, messageId: { type: String },
}); });
module.exports = mongoose.model('EmailLog', EmailLogSchema); module.exports = (conn) => {
return conn.model('EmailLog', EmailLogSchema);
};

@ -15,4 +15,6 @@ const EmailVerifySchema = new Schema({
token: { type: String, required: true }, token: { type: String, required: true },
}); });
module.exports = mongoose.model('EmailVerify', EmailVerifySchema); module.exports = (conn) => {
return conn.model('EmailVerify', EmailVerifySchema);
};

@ -17,4 +17,6 @@ const EmailSchema = new Schema({
content: { type: Schema.ObjectId, required: true, index: true, refPath: 'contentType' }, content: { type: Schema.ObjectId, required: true, index: true, refPath: 'contentType' },
}); });
module.exports = mongoose.model('Email', EmailSchema); module.exports = (conn) => {
return conn.model('Email', EmailSchema);
};

@ -36,4 +36,6 @@ const EmojiReactionSchema = new Schema({
timestamp: { type: Number }, timestamp: { type: Number },
}); });
module.exports = mongoose.model('EmojiReaction', EmojiReactionSchema); module.exports = (conn) => {
return conn.model('EmojiReaction', EmojiReactionSchema);
};

@ -23,4 +23,6 @@ FeedEntrySchema.index({
name: 'feed_entry_by_feed_idx', name: 'feed_entry_by_feed_idx',
}); });
module.exports = mongoose.model('FeedEntry', FeedEntrySchema); module.exports = (conn) => {
return conn.model('FeedEntry', FeedEntrySchema);
};

@ -18,4 +18,6 @@ const FeedSchema = new Schema({
published: { type: Date }, published: { type: Date },
}); });
module.exports = mongoose.model('Feed', FeedSchema); module.exports = (conn) => {
return conn.model('Feed', FeedSchema);
};

@ -32,4 +32,6 @@ const ImageSchema = new Schema({
}, },
}); });
module.exports = mongoose.model('Image', ImageSchema); module.exports = (conn) => {
return conn.model('Image', ImageSchema);
};

@ -57,4 +57,6 @@ KaleidoscopeEventSchema.index({
name: 'evtsrc_site_author_index', name: 'evtsrc_site_author_index',
}); });
module.exports = mongoose.model('KaleidoscopeEvent', KaleidoscopeEventSchema); module.exports = (conn) => {
return conn.model('KaleidoscopeEvent', KaleidoscopeEventSchema);
};

@ -29,4 +29,6 @@ const LogSchema = new Schema({
metadata: { type: Schema.Types.Mixed }, metadata: { type: Schema.Types.Mixed },
}); });
module.exports = mongoose.model('Log', LogSchema); module.exports = (conn) => {
return conn.model('Log', LogSchema);
};

@ -0,0 +1,55 @@
// media-router.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const STATUS_LIST = [
'starting', // the router process is starting and configuring itself
'active', // the router is active and available for service
'capacity', // the router is at or over capacity
'closing', // the router is closing/shutting down
'closed', // the router no longer exists
];
const RouterHostSchema = new Schema({
address: { type: String, required: true, index: 1 },
port: { type: Number, required: true },
});
/*
* A Router is a "multi-user conference call instance" somewhere on the
* infrastructure. This model helps us manage them, balance load across them,
* and route calls to and between them (for scale).
*
* These records are created when a call is being created, and are commonly
* left in the database after all call participants have left. An expires index
* is used to sweep up router records after 30 days. This allows us to perform
* statistics aggregation on router use and store aggregated results as part of
* long-term reporting.
*/
const MediaRouterSchema = new Schema({
created: { type: Date, default: Date.now, required: true, expires: '30d' },
lastActivity: { type: Date, default: Date.now, required: true },
status: { type: String, enum: STATUS_LIST, default: 'starting', required: true, index: true },
name: { type: String },
description: { type: String },
access: {
isPrivate: { type: Boolean, default: true, required: true },
passcodeHash: { type: String, select: false },
},
host: { type: RouterHostSchema, required: true, select: false },
stats: {
routerCount: { type: Number, default: 0, required: true },
consumerCount: { type: Number, default: 0, required: true },
producerCount: { type: Number, default: 0, required: true },
}
});
module.exports = (conn) => {
return conn.model('MediaRouter', MediaRouterSchema);
};

@ -0,0 +1,45 @@
// media-worker.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const STATUS_LIST = [
'starting', // the router process is starting and configuring itself
'active', // the router is active and available for service
'capacity', // the router is at or over capacity
'closing', // the router is closing/shutting down
'closed', // the router no longer exists
];
const WebRtcListenSchema = new Schema({
protocol: { type: String, enum: ['tcp','udp'], required: true },
ip: { type: String, required: true },
port: { type: Number, required: true },
});
/*
* A media worker is a host process with one or more MediaRouter instances
* processing multi-user conference calls.
*/
const MediaWorkerSchema = new Schema({
created: { type: Date, default: Date.now, required: true, expires: '30d' },
lastActivity: { type: Date, default: Date.now, required: true },
status: { type: String, enum: STATUS_LIST, default: 'starting', required: true, index: true },
webRtcServer: {
listenInfos: { type: [WebRtcListenSchema] },
},
stats: {
routerCount: { type: Number, default: 0, required: true },
consumerCount: { type: Number, default: 0, required: true },
producerCount: { type: Number, default: 0, required: true },
}
});
module.exports = (conn) => {
return conn.model('MediaWorker', MediaWorkerSchema);
};

@ -72,4 +72,6 @@ const NetHostStatsSchema = new Schema({
network: { type: [NetworkInterfaceStatsSchema], required: true }, network: { type: [NetworkInterfaceStatsSchema], required: true },
}); });
module.exports = mongoose.model('NetHostStats', NetHostStatsSchema); module.exports = (conn) => {
return conn.model('NetHostStats', NetHostStatsSchema);
};

@ -42,4 +42,6 @@ const NetHostSchema = new Schema({
network: { type: [NetworkInterfaceSchema] }, network: { type: [NetworkInterfaceSchema] },
}); });
module.exports = mongoose.model('NetHost', NetHostSchema); module.exports = (conn) => {
return conn.model('NetHost', NetHostSchema);
};

@ -18,4 +18,6 @@ const NewsletterRecipientSchema = new Schema({
}, },
}); });
module.exports = mongoose.model('NewsletterRecipient', NewsletterRecipientSchema); module.exports = (conn) => {
return conn.model('NewsletterRecipient', NewsletterRecipientSchema);
};

@ -28,4 +28,6 @@ const NewsletterSchema = new Schema({
}, },
}); });
module.exports = mongoose.model('Newsletter', NewsletterSchema); module.exports = (conn) => {
return conn.model('Newsletter', NewsletterSchema);
};

@ -16,4 +16,6 @@ const OAuth2AuthorizationCodeSchema = new Schema({
scopes: { type: [String], required: true }, scopes: { type: [String], required: true },
}); });
module.exports = mongoose.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema); module.exports = (conn) => {
return conn.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
};

@ -40,4 +40,6 @@ OAuth2ClientSchema.index({
unique: true, unique: true,
}); });
module.exports = mongoose.model('OAuth2Client', OAuth2ClientSchema); module.exports = (conn) => {
return conn.model('OAuth2Client', OAuth2ClientSchema);
};

@ -24,4 +24,6 @@ OAuth2TokenSchema.index({
name: 'oauth2_token_unique', name: 'oauth2_token_unique',
}); });
module.exports = mongoose.model('OAuth2Token', OAuth2TokenSchema); module.exports = (conn) => {
return conn.model('OAuth2Token', OAuth2TokenSchema);
};

@ -33,4 +33,6 @@ OtpAccountSchema.index({
name: 'otp_user_svc_uniq_idx', name: 'otp_user_svc_uniq_idx',
}); });
module.exports = mongoose.model('OtpAccount', OtpAccountSchema); module.exports = (conn) => {
return conn.model('OtpAccount', OtpAccountSchema);
};

@ -26,4 +26,6 @@ const PageSchema = new Schema({
}, },
}); });
module.exports = mongoose.model('Page', PageSchema); module.exports = (conn) => {
return conn.model('Page', PageSchema);
};

@ -43,4 +43,6 @@ PostSchema.index({
name: 'post_author_by_type', name: 'post_author_by_type',
}); });
module.exports = mongoose.model('Post', PostSchema); module.exports = (conn) => {
return conn.model('Post', PostSchema);
};

@ -29,4 +29,6 @@ ResourceViewSchema.index({
name: 'res_view_daily_unique', name: 'res_view_daily_unique',
}); });
module.exports = mongoose.model('ResourceView', ResourceViewSchema); module.exports = (conn) => {
return conn.model('ResourceView', ResourceViewSchema);
};

@ -26,4 +26,6 @@ ResourceVisitSchema.index({
name: 'resource_visits_for_user', name: 'resource_visits_for_user',
}); });
module.exports = mongoose.model('ResourceVisit', ResourceVisitSchema); module.exports = (conn) => {
return conn.model('ResourceVisit', ResourceVisitSchema);
};

@ -16,4 +16,6 @@ const SiteLinkSchema = new Schema({
target: { type: String }, target: { type: String },
}); });
module.exports = mongoose.model('SiteLink', SiteLinkSchema); module.exports = (conn) => {
return conn.model('SiteLink', SiteLinkSchema);
};

@ -43,4 +43,6 @@ const StickerSchema = new Schema({
encoded: { type: StickerMediaSchema }, encoded: { type: StickerMediaSchema },
}); });
module.exports = mongoose.model('Sticker', StickerSchema); module.exports = (conn) => {
return conn.model('Sticker', StickerSchema);
};

@ -12,4 +12,6 @@ const UserBlockSchema = new Schema({
blockedUsers: { type: [Schema.ObjectId], ref: 'User' }, blockedUsers: { type: [Schema.ObjectId], ref: 'User' },
}); });
module.exports = mongoose.model('UserBlock', UserBlockSchema); module.exports = (conn) => {
return conn.model('UserBlock', UserBlockSchema);
};

@ -24,4 +24,6 @@ const UserNotificationSchema = new Schema({
event: { type: Schema.ObjectId, required: true, ref: 'KaleidoscopeEvent' }, event: { type: Schema.ObjectId, required: true, ref: 'KaleidoscopeEvent' },
}); });
module.exports = mongoose.model('UserNotification', UserNotificationSchema); module.exports = (conn) => {
return conn.model('UserNotification', UserNotificationSchema);
};

@ -24,4 +24,6 @@ const UserSubscriptionSchema = new Schema({
subscriptions: { type: [SubscriptionSchema] }, subscriptions: { type: [SubscriptionSchema] },
}); });
module.exports = mongoose.model('UserSubscription', UserSubscriptionSchema); module.exports = (conn) => {
return conn.model('UserSubscription', UserSubscriptionSchema);
};

@ -22,17 +22,18 @@ const {
const UserSchema = new Schema({ const UserSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1 }, created: { type: Date, default: Date.now, required: true, index: -1 },
email: { type: String, required: true, lowercase: true, unique: true }, email: { type: String, required: true, lowercase: true, unique: true, select: false },
username: { type: String, required: true }, username: { type: String, required: true },
username_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 }, username_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 },
passwordSalt: { type: String, required: true }, passwordSalt: { type: String, required: true, select: false },
password: { type: String, required: true }, password: { type: String, required: true, select: false },
displayName: { type: String }, displayName: { type: String },
bio: { type: String, maxlength: 300 }, bio: { type: String, maxlength: 300 },
picture: { picture: {
large: { type: Schema.ObjectId, ref: 'Image' }, large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' }, small: { type: Schema.ObjectId, ref: 'Image' },
}, },
header: { type: Schema.ObjectId, ref: 'Image' },
badges: { type: [String] }, badges: { type: [String] },
flags: { type: UserFlagsSchema, select: false }, flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false }, permissions: { type: UserPermissionsSchema, select: false },
@ -58,4 +59,6 @@ UserSchema.virtual('hasAuthorDashboard').get( function ( ) {
this.flags.isAdmin; this.flags.isAdmin;
}); });
module.exports = mongoose.model('User', UserSchema); module.exports = (conn) => {
return conn.model('User', UserSchema);
};

@ -43,4 +43,6 @@ const VenueChannelStatusSchema = new Schema({
}, },
}); });
module.exports = mongoose.model('VenueChannelStatus', VenueChannelStatusSchema); module.exports = (conn) => {
return conn.model('VenueChannelStatus', VenueChannelStatusSchema);
};

@ -22,4 +22,6 @@ const VenueChannelSchema = new Schema({
credentials: { type: ChannelCredentialsSchema, required: true, select: false }, credentials: { type: ChannelCredentialsSchema, required: true, select: false },
}); });
module.exports = mongoose.model('VenueChannel', VenueChannelSchema); module.exports = (conn) => {
return conn.model('VenueChannel', VenueChannelSchema);
};

@ -118,5 +118,6 @@ class AnnouncementService extends SiteService {
module.exports = { module.exports = {
slug: 'announcement', slug: 'announcement',
name: 'announcement', name: 'announcement',
className: 'AnnouncementService',
create: (dtp) => { return new AnnouncementService(dtp); }, create: (dtp) => { return new AnnouncementService(dtp); },
}; };

@ -182,5 +182,6 @@ class AttachmentService extends SiteService {
module.exports = { module.exports = {
slug: 'attachment', slug: 'attachment',
name: 'attachment', name: 'attachment',
className: 'AttachmentService',
create: (dtp) => { return new AttachmentService(dtp); }, create: (dtp) => { return new AttachmentService(dtp); },
}; };

@ -59,5 +59,6 @@ class CacheService extends SiteService {
module.exports = { module.exports = {
slug: 'cache', slug: 'cache',
name: 'cache', name: 'cache',
className: 'CacheService',
create: (dtp) => { return new CacheService(dtp); }, create: (dtp) => { return new CacheService(dtp); },
}; };

@ -473,7 +473,12 @@ class ChatService extends SiteService {
async createMessage (author, messageDefinition) { async createMessage (author, messageDefinition) {
const { sticker: stickerService, user: userService } = this.dtp.services; const { sticker: stickerService, user: userService } = this.dtp.services;
author = await userService.getUserAccount(author._id); this.log.alert('user record', { author });
if (author.type === 'User') {
author = await userService.getLocalUserAccount(author._id);
} else {
author = await userService.getCoreUserAccount(author._id);
}
if (!author || !author.permissions || !author.permissions.canChat) { if (!author || !author.permissions || !author.permissions.canChat) {
throw new SiteError(403, `You are not permitted to chat at all on ${this.dtp.config.site.name}`); throw new SiteError(403, `You are not permitted to chat at all on ${this.dtp.config.site.name}`);
} }
@ -744,7 +749,13 @@ class ChatService extends SiteService {
const { user: userService } = this.dtp.services; const { user: userService } = this.dtp.services;
const NOW = new Date(); const NOW = new Date();
const userCheck = await userService.getUserAccount(user._id); let userCheck;
if (user.type === '') {
userCheck = await userService.getLocalUserAccount(user._id);
} else {
userCheck = await userService.getCoreUserAccount(user._id);
}
if (!userCheck || !userCheck.permissions || !userCheck.permissions.canChat) { if (!userCheck || !userCheck.permissions || !userCheck.permissions.canChat) {
throw new SiteError(403, 'You are not permitted to chat'); throw new SiteError(403, 'You are not permitted to chat');
} }
@ -773,10 +784,76 @@ class ChatService extends SiteService {
return reaction.toObject(); return reaction.toObject();
} }
async removeForUser (user) {
const { logan: loganService } = this.dtp.services;
let roomCount = 0, messageCount = 0;
await ChatRoom
.find({ owner: user._id })
.populate(this.populateChatRoom)
.cursor()
.eachAsync(async (room) => {
try {
await this.deleteRoom(room);
++roomCount;
} catch (error) {
loganService.sendEvent(module.exports, {
level: 'error',
event: 'removeForUser',
message: `failed to remove chat room: ${error.message}`,
data: {
room: {
_id: room._id,
name: room.name,
},
error,
},
});
// fall through
}
});
await ChatMessage
.find({ author: user._id })
.cursor()
.eachAsync(async (message) => {
try {
await this.removeMessage(message);
++messageCount;
} catch (error) {
loganService.sendEvent(module.exports, {
level: 'error',
event: 'removeForUser',
message: `failed to remove chat message: ${error.message}`,
data: {
message: {
_id: message._id,
},
error,
},
});
// fall through
}
});
loganService.sendEvent(module.exports, {
level: 'info',
event: 'removeForUser',
data: {
user: {
_id: user._id,
username: user.username,
},
roomCount, messageCount,
},
});
}
} }
module.exports = { module.exports = {
slug: 'chat', slug: 'chat',
name: 'chat', name: 'chat',
className: 'ChatService',
create: (dtp) => { return new ChatService(dtp); }, create: (dtp) => { return new ChatService(dtp); },
}; };

@ -320,10 +320,23 @@ class CommentService extends SiteService {
await Comment.deleteOne({ _id: comment._id }); await Comment.deleteOne({ _id: comment._id });
}, 4); }, 4);
} }
async removeForAuthor (author) {
await Comment
.find({ // index: 'comment_author_by_type'
authorType: author.type,
author: author._id,
})
.cursor()
.eachAsync(async (comment) => {
await this.remove(comment);
});
}
} }
module.exports = { module.exports = {
slug: 'comment', slug: 'comment',
name: 'comment', name: 'comment',
className: 'CommentService',
create: (dtp) => { return new CommentService(dtp); }, create: (dtp) => { return new CommentService(dtp); },
}; };

@ -117,6 +117,10 @@ class ContentReportService extends SiteService {
await ContentReport.deleteMany({ resource: resource._id }); await ContentReport.deleteMany({ resource: resource._id });
} }
async removeForUser (user) {
await ContentReport.deleteMany({ user: user._id });
}
async removeReport (report) { async removeReport (report) {
await ContentReport.deleteOne({ _id: report._id }); await ContentReport.deleteOne({ _id: report._id });
} }
@ -125,5 +129,6 @@ class ContentReportService extends SiteService {
module.exports = { module.exports = {
slug: 'content-report', slug: 'content-report',
name: 'contentReport', name: 'contentReport',
className: 'ContentReportService',
create: (dtp) => { return new ContentReportService(dtp); }, create: (dtp) => { return new ContentReportService(dtp); },
}; };

@ -95,10 +95,30 @@ class ContentVoteService extends SiteService {
this.log.info('removing all votes for resource', { resourceId: resource._id }); this.log.info('removing all votes for resource', { resourceId: resource._id });
await ContentVote.deleteMany({ resource: resource._id }); await ContentVote.deleteMany({ resource: resource._id });
} }
async removeForUser (user) {
await ContentVote
.find({ user: user._id })
.cursor()
.eachAsync(async (vote) => {
const updateOp = { $inc: { } };
if (vote.vote === 'up') {
updateOp.$inc['resourceStats.upvoteCount'] = -1;
} else {
updateOp.$inc['resourceStats.downvoteCount'] = -1;
}
const resourceModel = mongoose.model(vote.resourceType);
await resourceModel.updateOne({ _id: vote.resource }, updateOp);
});
await ContentVote.deleteMany({ user: user._id });
}
} }
module.exports = { module.exports = {
slug: 'content-vote', slug: 'content-vote',
name: 'contentVote', name: 'contentVote',
className: 'ContentVoteService',
create: (dtp) => { return new ContentVoteService(dtp); }, create: (dtp) => { return new ContentVoteService(dtp); },
}; };

@ -718,5 +718,6 @@ class CoreNodeService extends SiteService {
module.exports = { module.exports = {
slug: 'core-node', slug: 'core-node',
name: 'coreNode', name: 'coreNode',
className: 'CoreNodeService',
create: (dtp) => { return new CoreNodeService(dtp); }, create: (dtp) => { return new CoreNodeService(dtp); },
}; };

@ -60,5 +60,6 @@ class CryptoService extends SiteService {
module.exports = { module.exports = {
slug: 'crypto', slug: 'crypto',
name: 'crypto', name: 'crypto',
className: 'CryptoService',
create: (dtp) => { return new CryptoService(dtp); }, create: (dtp) => { return new CryptoService(dtp); },
}; };

@ -69,10 +69,15 @@ class CsrfTokenService extends SiteService {
csrfToken.name = `csrf-token-${options.name}`; csrfToken.name = `csrf-token-${options.name}`;
return csrfToken; return csrfToken;
} }
async removeForUser (user) {
await CsrfToken.deleteMany({ user: user._id });
}
} }
module.exports = { module.exports = {
slug: 'csrf-token', slug: 'csrf-token',
name: 'csrfToken', name: 'csrfToken',
className: 'CsrfTokenService',
create: (dtp) => { return new CsrfTokenService(dtp); }, create: (dtp) => { return new CsrfTokenService(dtp); },
}; };

@ -272,5 +272,6 @@ class DashboardService extends SiteService {
module.exports = { module.exports = {
slug: 'dashboard', slug: 'dashboard',
name: 'dashboard', name: 'dashboard',
className: 'DashboardService',
create: (dtp) => { return new DashboardService(dtp); }, create: (dtp) => { return new DashboardService(dtp); },
}; };

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

Loading…
Cancel
Save