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_DATABASE=dtp-sites
MONGODB_USERNAME=mongo-user
MONGODB_PASSWORD=change-me!
MONGODB_OPTIONS=
#
# 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
```
## 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
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,
dashboard: dashboardService,
venue: venueService,
logan: loganService,
} = this.dtp.services;
res.locals.stats = {
@ -92,6 +93,11 @@ class AdminController extends SiteController {
res.locals.channels = await venueService.getChannels();
res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHomeView',
});
res.render('admin/index');
}
}
@ -99,5 +105,6 @@ class AdminController extends SiteController {
module.exports = {
slug: 'admin',
name: 'admin',
className: 'AdminController',
create: async (dtp) => { return new AdminController(dtp); },
};

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

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController } = require('../../../lib/site-lib');
class ContentReportController extends SiteController {
class ContentReportAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -89,5 +89,6 @@ class ContentReportController extends SiteController {
module.exports = {
name: 'adminContentReport',
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');
class CoreNodeController extends SiteController {
class CoreNodeAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -142,5 +142,6 @@ class CoreNodeController extends SiteController {
module.exports = {
name: 'adminCoreNode',
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');
class CoreUserController extends SiteController {
class CoreUserAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -90,5 +90,6 @@ class CoreUserController extends SiteController {
module.exports = {
name: 'adminCoreUser',
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');
class HostController extends SiteController {
class HostAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -118,5 +118,6 @@ class HostController extends SiteController {
module.exports = {
name: 'adminHost',
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');
class JobQueueController extends SiteController {
class JobQueueAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -121,5 +121,6 @@ class JobQueueController extends SiteController {
module.exports = {
name: 'adminJobQueue',
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');
class LogController extends SiteController {
class LogAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -53,5 +53,6 @@ class LogController extends SiteController {
module.exports = {
name: 'adminLog',
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');
class NewsletterController extends SiteController {
class NewsletterAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -169,8 +169,6 @@ class NewsletterController extends SiteController {
module.exports = {
name: 'adminNewsletter',
slug: 'admin-newsletter',
create: async (dtp) => {
let controller = new NewsletterController(dtp);
return controller;
},
className: 'NewsletterAdminController',
create: async (dtp) => { return new NewsletterAdminController(dtp); },
};

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

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

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

@ -101,7 +101,9 @@ class PostController extends SiteController {
try {
res.locals.pageTitle = `Posts for ${this.dtp.config.site.name}`;
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');
} catch (error) {
this.log.error('failed to fetch posts', { error });
@ -140,5 +142,6 @@ class PostController extends SiteController {
module.exports = {
name: 'adminPost',
slug: 'admin-post',
className: 'PostController',
create: async (dtp) => { return new PostController(dtp); },
};

@ -8,7 +8,7 @@ const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class ServiceNodeController extends SiteController {
class ServiceNodeAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -130,5 +130,6 @@ class ServiceNodeController extends SiteController {
module.exports = {
name: 'adminServiceNode',
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');
class SettingsController extends SiteController {
class SettingsAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
@ -127,5 +127,6 @@ class SettingsController extends SiteController {
module.exports = {
name: 'adminSettings',
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 = {
name: 'adminSiteLink',
slug: 'admin-site-link',
className: 'SiteLinkAdminController',
create: async (dtp) => { return new SiteLinkAdminController(dtp); },
};

@ -6,9 +6,9 @@
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) {
super(dtp, module.exports);
@ -22,37 +22,73 @@ class UserController extends SiteController {
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));
return router;
}
async populateUserId (req, res, next, userId) {
async populateLocalUserId (req, res, next, localUserId) {
const { user: userService } = this.dtp.services;
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();
} catch (error) {
return next(error);
}
}
async postUpdateUser (req, res, next) {
const { user: userService } = this.dtp.services;
async postUpdateLocalUser (req, res, next) {
const {
logan: loganService,
user: userService,
} = this.dtp.services;
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');
} catch (error) {
return next(error);
}
}
async getUserView (req, res, next) {
async getLocalUserView (req, res, next) {
const { comment: commentService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
@ -68,7 +104,7 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services;
try {
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.render('admin/user/index');
} catch (error) {
@ -80,5 +116,6 @@ class UserController extends SiteController {
module.exports = {
name: 'adminUser',
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 = {
name: 'adminVenue',
slug: 'admin-venue',
className: 'VenueAdminController',
create: async (dtp) => { return new VenueAdminController(dtp); },
};

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

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

@ -26,15 +26,11 @@ class AuthorController extends SiteController {
return res.redirect(302, '/welcome');
}
if (req.user.flags.isAdmin) {
return next();
}
const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts;
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();
}
@ -69,29 +65,35 @@ class AuthorController extends SiteController {
async getPublishedPostHome (req, res, next) {
const { post: postService } = this.dtp.services;
try {
const isAdmin = req.user.flags.isAdmin;
const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts;
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.published = { };
if(canAuthor) {
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.published.all = true;
res.locals.published.posts.all = true;
} 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 ) {
res.locals.published = await postService.getPosts( { skip: 0, cpp: 5 }, ['published'], true );
else if ( canPublish) {
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;
}
@ -105,31 +107,37 @@ class AuthorController extends SiteController {
async getDraftsHome (req, res, next) {
const { post: postService } = this.dtp.services;
try {
const isAdmin = req.user.flags.isAdmin;
const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts;
res.locals.pagination = this.getPaginationParameters(req, 20);
const status = ['draft'];
res.locals.drafts = { };
if(canAuthor) {
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.drafts.all = true;
res.locals.drafts.posts.all = true;
} 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 ) {
res.locals.drafts = await postService.getPosts( { skip: 0, cpp: 5 }, status, true );
res.locals.drafts.all = true;
else if ( canPublish) {
const {posts, totalPostCount }= await postService.getPosts( res.locals.pagination, status, 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');

@ -132,7 +132,10 @@ class ChatController extends SiteController {
}
async populateRoomId (req, res, next, roomId) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.room = await chatService.getRoomById(roomId);
if (!res.locals.room) {
@ -141,12 +144,21 @@ class ChatController extends SiteController {
return next();
} catch (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);
}
}
async populateInviteId (req, res, next, inviteId) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.invite = await chatService.getRoomInviteById(inviteId);
if (!res.locals.invite) {
@ -155,12 +167,21 @@ class ChatController extends SiteController {
return next();
} catch (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);
}
}
async postRoomInviteAction (req, res) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
const { response } = req.body;
const displayList = this.createDisplayList('room-invite-action');
@ -168,11 +189,27 @@ class ChatController extends SiteController {
switch (response) {
case 'accept':
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}`);
break;
case 'reject':
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(
`Chat room invite rejected`,
'success',
@ -192,6 +229,12 @@ class ChatController extends SiteController {
response: req.body.response,
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({
success: false,
message: error.message,
@ -200,7 +243,11 @@ class ChatController extends SiteController {
}
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 });
if (!req.body.username || !req.body.username.length) {
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.");
}
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');
displayList.showNotification(
@ -232,9 +279,28 @@ class ChatController extends SiteController {
'top-left',
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 });
} catch (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({
success: false,
message: error.message,
@ -243,35 +309,78 @@ class ChatController extends SiteController {
}
async postRoomUpdate (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
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}`);
} catch (error) {
this.log.error('failed to update chat room', {
// roomId: res.locals.room._id,
error,
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postRoomUpdate',
message: `failed to update chat room: ${error.message}`,
data: { error },
});
return next(error);
}
}
async postRoomCreate (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
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}`);
} 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);
}
}
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');
}
async getRoomForm (req, res, next) {
const { logan: loganService } = this.dtp.services;
const validFormNames = [
'invite-member',
];
@ -280,65 +389,148 @@ class ChatController extends SiteController {
return next(new SiteError(404, 'Form not found'));
}
try {
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomForm',
data: { formName },
});
res.render(`chat/room/form/${formName}`);
} catch (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);
}
}
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');
}
async getRoomInviteHome (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.invites = {
new: await chatService.getRoomInvites(res.locals.room, 'new'),
accepted: await chatService.getRoomInvites(res.locals.room, 'accepted'),
rejected: await chatService.getRoomInvites(res.locals.room, 'rejected'),
};
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getRoomInviteHome',
});
res.render('chat/room/invite');
} 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);
}
}
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');
}
async getRoomView (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.pageTitle = res.locals.room.name;
const pagination = { skip: 0, cpp: 20 };
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');
} catch (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);
}
}
async getRoomHome (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
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');
} 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);
}
}
async getHome (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.pageTitle = 'Chat Home';
@ -349,15 +541,28 @@ class ChatController extends SiteController {
res.locals.joinedChatRooms.forEach((room) => roomIds.push(room._id));
res.locals.timeline = await chatService.getMultiRoomTimeline(roomIds, res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHome',
});
res.render('chat/index');
} 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);
}
}
async deleteInvite (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
if (res.locals.room.owner._id.equals(req.user._id)) {
throw new SiteError(403, 'This is not your invitiation');
@ -374,15 +579,34 @@ class ChatController extends SiteController {
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 });
} 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);
}
}
async deleteRoom (req, res, next) {
const { chat: chatService } = this.dtp.services;
const {
chat: chatService,
logan: loganService,
} = this.dtp.services;
try {
if (res.locals.room.owner._id.equals(req.user._id)) {
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');
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 });
} 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);
}
}
@ -404,5 +645,6 @@ class ChatController extends SiteController {
module.exports = {
slug: 'chat',
name: 'chat',
className: 'ChatController',
create: async (dtp) => { return new ChatController(dtp); },
};

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

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

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

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

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

@ -78,7 +78,7 @@ class HiveUserController extends SiteController {
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.userProfiles = await userService.getUserAccounts(res.locals.pagination, res.locals.q);
res.locals.userProfiles = res.locals.userProfiles.map((user) => {
@ -151,8 +151,6 @@ class HiveUserController extends SiteController {
module.exports = {
name: 'hiveUser',
slug: 'hive-user',
create: async (dtp) => {
let controller = new HiveUserController(dtp);
return controller;
},
className: 'HiveUserController',
create: async (dtp) => { return new HiveUserController(dtp); },
};

@ -63,7 +63,12 @@ class HomeController extends SiteController {
}
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 {
res.locals.announcements = await announcementService.getLatest(req.user);
res.locals.featuredPosts = await postService.getFeaturedPosts(3);
@ -72,17 +77,28 @@ class HomeController extends SiteController {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getHome',
});
res.render('index');
} 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);
}
}
}
module.exports = {
isHome: true,
slug: 'home',
name: 'home',
isHome: true,
className: 'HomeController',
create: async (dtp) => { return new HomeController(dtp); },
};

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

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

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

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

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

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

@ -33,8 +33,8 @@ class PostController extends SiteController {
if (req.user && req.user.flags.isAdmin) {
return next();
}
if (!req.user || !req.flags.isAdmin) {
return next(new SiteError(403, 'Author or admin privileges are required'));
if (!req.user || !req.user.permissions.canAuthorPages) {
return next(new SiteError(403, 'Author privileges are required'));
}
return next();
}
@ -43,11 +43,12 @@ class PostController extends SiteController {
res.locals.currentView = 'home';
return next();
});
router.param('username', this.populateUsername.bind(this));
router.param('postSlug', this.populatePostSlug.bind(this));
router.param('postId', this.populatePostId.bind(this));
router.param('tagSlug', this.populateTagSlug.bind(this));
router.param('commentId', commentService.populateCommentId.bind(commentService));
router.post('/:postSlug/comment/:commentId/block-author', authRequired, upload.none(), this.postBlockCommentAuthor.bind(this));
@ -72,12 +73,14 @@ class PostController extends SiteController {
limiterService.createMiddleware(limiterService.config.post.getIndex),
this.getAuthorView.bind(this),
);
router.get('/authors',
limiterService.createMiddleware(limiterService.config.post.getAllAuthorsView),
this.getAllAuthorsView.bind(this),
limiterService.createMiddleware(limiterService.config.post.getAllAuthorsView),
this.getAllAuthorsView.bind(this),
);
router.get('/tags', this.getTagIndex.bind(this));
router.get('/:postSlug',
limiterService.createMiddleware(limiterService.config.post.getView),
this.getView.bind(this),
@ -101,7 +104,9 @@ class PostController extends SiteController {
this.deletePost.bind(this),
);
router.get('/*', this.badRoute.bind(this) );
router.get('/tag/:tagSlug', this.getTagSearchView.bind(this));
}
async populateUsername (req, res, next, username) {
@ -319,7 +324,7 @@ class PostController extends SiteController {
res.status(200).json({ success: true, displayList });
} 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({
success: false,
message: error.message,
@ -338,9 +343,9 @@ class PostController extends SiteController {
throw new SiteError(403, 'The post is not published');
}
}
await resourceService.recordView(req, 'Post', res.locals.post._id);
if (res.locals.post.status === 'published') {
await resourceService.recordView(req, 'Post', res.locals.post._id);
}
res.locals.countPerPage = 20;
res.locals.pagination = this.getPaginationParameters(req, res.locals.countPerPage);
@ -409,9 +414,9 @@ class PostController extends SiteController {
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
const {authors , totalAuthorCount }= await userService.getAuthors(res.locals.pagination);
res.locals.authors = authors;
res.locals.totalAuthorCount = totalAuthorCount;
const {authors , totalAuthorCount } = await userService.getAuthors(res.locals.pagination);
res.locals.authors = authors;
res.locals.totalAuthorCount = totalAuthorCount;
res.render('post/author/all');
} catch (error) {
return next(error);
@ -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 = {
slug: 'post',
name: 'post',
create: async (dtp) => {
let controller = new PostController(dtp);
return controller;
},
className: 'PostController',
create: async (dtp) => { return new PostController(dtp); },
};

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

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

@ -65,23 +65,63 @@ class WelcomeController extends SiteController {
}
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.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');
}
async getLoginView (req, res) {
const { logan: loganService } = this.dtp.services;
res.locals.loginResult = req.session.loginResult;
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'getLoginView',
message: 'serving member login view',
});
res.render('welcome/login');
}
async getHomeView (req, res) {
res.render('welcome/index');
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');
} 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 = {
slug: 'welcome',
name: 'welcome',
className: 'WelcomeController',
create: async (dtp) => { return new WelcomeController(dtp); },
};
};

@ -29,4 +29,6 @@ const AnnouncementSchema = new Schema({
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',
});
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' },
});
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',
});
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',
});
module.exports = mongoose.model('ChatRoom', ChatRoomSchema);
module.exports = (conn) => {
return conn.model('ChatRoom', ChatRoomSchema);
};

@ -72,4 +72,6 @@ CommentSchema.index({
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 },
});
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',
});
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',
});
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',
});
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',
});
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 },
});
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 },
});
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 },
});
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 },
});
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' },
});
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 },
});
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',
});
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 },
});
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',
});
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 },
});
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 },
});
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] },
});
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 },
});
module.exports = mongoose.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
module.exports = (conn) => {
return conn.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
};

@ -40,4 +40,6 @@ OAuth2ClientSchema.index({
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',
});
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',
});
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',
});
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',
});
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',
});
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 },
});
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 },
});
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' },
});
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' },
});
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] },
});
module.exports = mongoose.model('UserSubscription', UserSubscriptionSchema);
module.exports = (conn) => {
return conn.model('UserSubscription', UserSubscriptionSchema);
};

@ -22,17 +22,18 @@ const {
const UserSchema = new Schema({
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_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 },
passwordSalt: { type: String, required: true },
password: { type: String, required: true },
passwordSalt: { type: String, required: true, select: false },
password: { type: String, required: true, select: false },
displayName: { type: String },
bio: { type: String, maxlength: 300 },
picture: {
large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' },
},
header: { type: Schema.ObjectId, ref: 'Image' },
badges: { type: [String] },
flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false },
@ -58,4 +59,6 @@ UserSchema.virtual('hasAuthorDashboard').get( function ( ) {
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 },
});
module.exports = mongoose.model('VenueChannel', VenueChannelSchema);
module.exports = (conn) => {
return conn.model('VenueChannel', VenueChannelSchema);
};

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

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

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

@ -473,7 +473,12 @@ class ChatService extends SiteService {
async createMessage (author, messageDefinition) {
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) {
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 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) {
throw new SiteError(403, 'You are not permitted to chat');
}
@ -773,10 +784,76 @@ class ChatService extends SiteService {
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 = {
slug: 'chat',
name: 'chat',
className: 'ChatService',
create: (dtp) => { return new ChatService(dtp); },
};

@ -320,10 +320,23 @@ class CommentService extends SiteService {
await Comment.deleteOne({ _id: comment._id });
}, 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 = {
slug: 'comment',
name: 'comment',
className: 'CommentService',
create: (dtp) => { return new CommentService(dtp); },
};

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

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

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

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

@ -272,5 +272,6 @@ class DashboardService extends SiteService {
module.exports = {
slug: 'dashboard',
name: 'dashboard',
className: 'DashboardService',
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