master
commit
0dd25e5176
@ -0,0 +1,60 @@
|
|||||||
|
// admin/content-report.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const DTP_COMPONENT_NAME = 'admin:content-report';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
// const multer = require('multer');
|
||||||
|
|
||||||
|
const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib');
|
||||||
|
|
||||||
|
class CoreNodeController extends SiteController {
|
||||||
|
|
||||||
|
constructor (dtp) {
|
||||||
|
super(dtp, DTP_COMPONENT_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start ( ) {
|
||||||
|
// const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/upload` });
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
router.use(async (req, res, next) => {
|
||||||
|
res.locals.currentView = 'admin';
|
||||||
|
res.locals.adminView = 'core-node';
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/connect', this.postCoreNodeConnect.bind(this));
|
||||||
|
router.get('/connect', this.getCoreNodeConnectForm.bind(this));
|
||||||
|
|
||||||
|
router.get('/', this.getIndex.bind(this));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
async postCoreNodeConnect (req, res, next) {
|
||||||
|
// const { coreNode: coreNodeService } = this.dtp.services;
|
||||||
|
try {
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to create Core Node connection request', { error });
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCoreNodeConnectForm (req, res) {
|
||||||
|
res.render('admin/core-node/connect');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIndex (req, res) {
|
||||||
|
res.render('admin/core-node/index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (dtp) => {
|
||||||
|
let controller = new CoreNodeController(dtp);
|
||||||
|
return controller;
|
||||||
|
};
|
@ -1,123 +0,0 @@
|
|||||||
// admin/newsletter.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const DTP_COMPONENT_NAME = 'admin:newsletter';
|
|
||||||
const express = require('express');
|
|
||||||
|
|
||||||
const { SiteController, SiteError } = require('../../../lib/site-lib');
|
|
||||||
|
|
||||||
class NewsletterController extends SiteController {
|
|
||||||
|
|
||||||
constructor (dtp) {
|
|
||||||
super(dtp, DTP_COMPONENT_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
async start ( ) {
|
|
||||||
const router = express.Router();
|
|
||||||
router.use(async (req, res, next) => {
|
|
||||||
res.locals.currentView = 'admin';
|
|
||||||
res.locals.adminView = 'newsletter';
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.param('newsletterId', this.populateNewsletterId.bind(this));
|
|
||||||
|
|
||||||
router.post('/:newsletterId', this.postUpdateNewsletter.bind(this));
|
|
||||||
router.post('/', this.postCreateNewsletter.bind(this));
|
|
||||||
|
|
||||||
router.get('/compose', this.getComposer.bind(this));
|
|
||||||
router.get('/:newsletterId', this.getComposer.bind(this));
|
|
||||||
|
|
||||||
router.get('/', this.getIndex.bind(this));
|
|
||||||
|
|
||||||
router.delete('/:newsletterId', this.deleteNewsletter.bind(this));
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
async populateNewsletterId (req, res, next, newsletterId) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.newsletter = await newsletterService.getById(newsletterId);
|
|
||||||
if (!res.locals.newsletter) {
|
|
||||||
throw new SiteError(404, 'Newsletter not found');
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to populate newsletterId', { newsletterId, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postUpdateNewsletter (req, res, next) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
await newsletterService.update(res.locals.newsletter, req.body);
|
|
||||||
res.redirect('/admin/newsletter');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to update newsletter', { newletterId: res.locals.newsletter._id, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postCreateNewsletter (req, res, next) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
await newsletterService.create(req.user, req.body);
|
|
||||||
res.redirect('/admin/newsletter');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to create newsletter', { error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getComposer (req, res) {
|
|
||||||
res.render('admin/newsletter/editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIndex (req, res, next) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.pagination = this.getPaginationParameters(req, 20);
|
|
||||||
res.locals.newsletters = await newsletterService.getNewsletters(res.locals.pagination, ['draft', 'published']);
|
|
||||||
res.render('admin/newsletter/index');
|
|
||||||
} catch (error) {
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNewsletter (req, res) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('delete-newsletter');
|
|
||||||
|
|
||||||
await newsletterService.deleteNewsletter(res.locals.newsletter);
|
|
||||||
|
|
||||||
displayList.removeElement(`li[data-newsletter-id="${res.locals.newsletter._id}"]`);
|
|
||||||
displayList.showNotification(
|
|
||||||
`Newsletter "${res.locals.newsletter.title}" deleted`,
|
|
||||||
'success',
|
|
||||||
'bottom-center',
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to delete newsletter', {
|
|
||||||
newsletterId: res.local.newsletter._id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
res.status(error.statusCode || 500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async (dtp) => {
|
|
||||||
let controller = new NewsletterController(dtp);
|
|
||||||
return controller;
|
|
||||||
};
|
|
@ -1,135 +0,0 @@
|
|||||||
// admin/page.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const DTP_COMPONENT_NAME = 'admin:page';
|
|
||||||
const express = require('express');
|
|
||||||
|
|
||||||
const { SiteController, SiteError } = require('../../../lib/site-lib');
|
|
||||||
|
|
||||||
class PageController extends SiteController {
|
|
||||||
|
|
||||||
constructor (dtp) {
|
|
||||||
super(dtp, DTP_COMPONENT_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
async start ( ) {
|
|
||||||
const router = express.Router();
|
|
||||||
router.use(async (req, res, next) => {
|
|
||||||
res.locals.currentView = 'admin';
|
|
||||||
res.locals.adminView = 'page';
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.param('pageId', this.populatePageId.bind(this));
|
|
||||||
|
|
||||||
router.post('/:pageId', this.pageUpdatePage.bind(this));
|
|
||||||
router.post('/', this.pageCreatePage.bind(this));
|
|
||||||
|
|
||||||
router.get('/compose', this.getComposer.bind(this));
|
|
||||||
router.get('/:pageId', this.getComposer.bind(this));
|
|
||||||
|
|
||||||
router.get('/', this.getIndex.bind(this));
|
|
||||||
|
|
||||||
router.delete('/:pageId', this.deletePage.bind(this));
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
async populatePageId (req, res, next, pageId) {
|
|
||||||
const { page: pageService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.page = await pageService.getById(pageId);
|
|
||||||
if (!res.locals.page) {
|
|
||||||
throw new SiteError(404, 'Page not found');
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to populate pageId', { pageId, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async pageUpdatePage (req, res, next) {
|
|
||||||
const { page: pageService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
await pageService.update(res.locals.page, req.body);
|
|
||||||
res.redirect('/admin/page');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to update page', { newletterId: res.locals.page._id, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async pageCreatePage (req, res, next) {
|
|
||||||
const { page: pageService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
await pageService.create(req.user, req.body);
|
|
||||||
res.redirect('/admin/page');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to create page', { error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getComposer (req, res, next) {
|
|
||||||
const { page: pageService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
let excludedPages;
|
|
||||||
if (res.locals.page) {
|
|
||||||
excludedPages = [res.locals.page._id];
|
|
||||||
}
|
|
||||||
res.locals.availablePages = await pageService.getAvailablePages(excludedPages);
|
|
||||||
res.render('admin/page/editor');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to serve page editor', { error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIndex (req, res, next) {
|
|
||||||
const { page: pageService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.pagination = this.getPaginationParameters(req, 20);
|
|
||||||
res.locals.pages = await pageService.getPages(res.locals.pagination, ['draft', 'published', 'archived']);
|
|
||||||
res.render('admin/page/index');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to fetch pages', { error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deletePage (req, res) {
|
|
||||||
const { page: pageService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('delete-page');
|
|
||||||
|
|
||||||
await pageService.deletePage(res.locals.page);
|
|
||||||
|
|
||||||
displayList.removeElement(`li[data-page-id="${res.locals.page._id}"]`);
|
|
||||||
displayList.showNotification(
|
|
||||||
`Page "${res.locals.page.title}" deleted`,
|
|
||||||
'success',
|
|
||||||
'bottom-center',
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to delete page', {
|
|
||||||
pageId: res.local.page._id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
res.status(error.statusCode || 500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async (dtp) => {
|
|
||||||
let controller = new PageController(dtp);
|
|
||||||
return controller;
|
|
||||||
};
|
|
@ -1,152 +0,0 @@
|
|||||||
// admin/post.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const DTP_COMPONENT_NAME = 'admin:post';
|
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const multer = require('multer');
|
|
||||||
|
|
||||||
const { SiteController, SiteError } = require('../../../lib/site-lib');
|
|
||||||
|
|
||||||
class PostController extends SiteController {
|
|
||||||
|
|
||||||
constructor (dtp) {
|
|
||||||
super(dtp, DTP_COMPONENT_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
async start ( ) {
|
|
||||||
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}` });
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
router.use(async (req, res, next) => {
|
|
||||||
res.locals.currentView = 'admin';
|
|
||||||
res.locals.adminView = 'post';
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.param('postId', this.populatePostId.bind(this));
|
|
||||||
|
|
||||||
router.post('/:postId/image', upload.single('imageFile'), this.postUpdateImage.bind(this));
|
|
||||||
router.post('/:postId', this.postUpdatePost.bind(this));
|
|
||||||
router.post('/', this.postCreatePost.bind(this));
|
|
||||||
|
|
||||||
router.get('/compose', this.getComposer.bind(this));
|
|
||||||
router.get('/:postId', this.getComposer.bind(this));
|
|
||||||
|
|
||||||
router.get('/', this.getIndex.bind(this));
|
|
||||||
|
|
||||||
router.delete('/:postId', this.deletePost.bind(this));
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
async populatePostId (req, res, next, postId) {
|
|
||||||
const { post: postService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.post = await postService.getById(postId);
|
|
||||||
if (!res.locals.post) {
|
|
||||||
throw new SiteError(404, 'Post not found');
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to populate postId', { postId, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postUpdateImage (req, res) {
|
|
||||||
const { post: postService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('post-image');
|
|
||||||
|
|
||||||
await postService.updateImage(req.user, res.locals.post, req.file);
|
|
||||||
|
|
||||||
displayList.showNotification(
|
|
||||||
'Profile photo updated successfully.',
|
|
||||||
'success',
|
|
||||||
'bottom-center',
|
|
||||||
2000,
|
|
||||||
);
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to update feature image', { error });
|
|
||||||
return res.status(error.statusCode || 500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postUpdatePost (req, res, next) {
|
|
||||||
const { post: postService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
await postService.update(res.locals.post, req.body);
|
|
||||||
res.redirect('/admin/post');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to update post', { newletterId: res.locals.post._id, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postCreatePost (req, res, next) {
|
|
||||||
const { post: postService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
await postService.create(req.user, req.body);
|
|
||||||
res.redirect('/admin/post');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to create post', { error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getComposer (req, res) {
|
|
||||||
res.render('admin/post/editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIndex (req, res, next) {
|
|
||||||
const { post: postService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.pagination = this.getPaginationParameters(req, 20);
|
|
||||||
res.locals.posts = await postService.getAllPosts(res.locals.pagination);
|
|
||||||
res.render('admin/post/index');
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to fetch posts', { error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deletePost (req, res) {
|
|
||||||
const { post: postService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('delete-post');
|
|
||||||
|
|
||||||
await postService.deletePost(res.locals.post);
|
|
||||||
|
|
||||||
displayList.removeElement(`li[data-post-id="${res.locals.post._id}"]`);
|
|
||||||
displayList.showNotification(
|
|
||||||
`Post "${res.locals.post.title}" deleted`,
|
|
||||||
'success',
|
|
||||||
'bottom-center',
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to delete post', {
|
|
||||||
postId: res.local.post._id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
res.status(error.statusCode || 500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async (dtp) => {
|
|
||||||
let controller = new PostController(dtp);
|
|
||||||
return controller;
|
|
||||||
};
|
|
@ -1,158 +0,0 @@
|
|||||||
// comment.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const DTP_COMPONENT_NAME = 'comment';
|
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const numeral = require('numeral');
|
|
||||||
|
|
||||||
const { SiteController, SiteError } = require('../../lib/site-lib');
|
|
||||||
|
|
||||||
class CommentController extends SiteController {
|
|
||||||
|
|
||||||
constructor (dtp) {
|
|
||||||
super(dtp, DTP_COMPONENT_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
async start ( ) {
|
|
||||||
const { dtp } = this;
|
|
||||||
const { limiter: limiterService, session: sessionService } = dtp.services;
|
|
||||||
|
|
||||||
const authRequired = sessionService.authCheckMiddleware({ requiredLogin: true });
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
dtp.app.use('/comment', router);
|
|
||||||
|
|
||||||
router.use(async (req, res, next) => {
|
|
||||||
res.locals.currentView = DTP_COMPONENT_NAME;
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.param('commentId', this.populateCommentId.bind(this));
|
|
||||||
|
|
||||||
router.post('/:commentId/vote', authRequired, this.postVote.bind(this));
|
|
||||||
|
|
||||||
router.get('/:commentId/replies', this.getCommentReplies.bind(this));
|
|
||||||
|
|
||||||
router.delete('/:commentId',
|
|
||||||
authRequired,
|
|
||||||
limiterService.create(limiterService.config.comment.deleteComment),
|
|
||||||
this.deleteComment.bind(this),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async populateCommentId (req, res, next, commentId) {
|
|
||||||
const { comment: commentService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.comment = await commentService.getById(commentId);
|
|
||||||
if (!res.locals.comment) {
|
|
||||||
return next(new SiteError(404, 'Comment not found'));
|
|
||||||
}
|
|
||||||
res.locals.post = res.locals.comment.resource;
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to populate commentId', { commentId, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postVote (req, res) {
|
|
||||||
const { contentVote: contentVoteService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('comment-vote');
|
|
||||||
const { message, stats } = await contentVoteService.recordVote(req.user, 'Comment', res.locals.comment, req.body.vote);
|
|
||||||
displayList.setTextContent(
|
|
||||||
`button[data-comment-id="${res.locals.comment._id}"][data-vote="up"] span.dtp-item-value`,
|
|
||||||
numeral(stats.upvoteCount).format(stats.upvoteCount > 1000 ? '0,0.0a' : '0,0'),
|
|
||||||
);
|
|
||||||
displayList.setTextContent(
|
|
||||||
`button[data-comment-id="${res.locals.comment._id}"][data-vote="down"] span.dtp-item-value`,
|
|
||||||
numeral(stats.downvoteCount).format(stats.upvoteCount > 1000 ? '0,0.0a' : '0,0'),
|
|
||||||
);
|
|
||||||
displayList.showNotification(message, 'success', 'bottom-center', 3000);
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to process comment vote', { error });
|
|
||||||
return res.status(error.statusCode || 500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCommentReplies (req, res) {
|
|
||||||
const { comment: commentService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('get-replies');
|
|
||||||
|
|
||||||
if (req.query.buttonId) {
|
|
||||||
displayList.removeElement(`li.dtp-load-more[data-button-id="${req.query.buttonId}"]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(res.locals, req.app.locals);
|
|
||||||
|
|
||||||
res.locals.countPerPage = parseInt(req.query.cpp || "20", 10);
|
|
||||||
if (res.locals.countPerPage < 1) {
|
|
||||||
res.locals.countPerPage = 1;
|
|
||||||
}
|
|
||||||
if (res.locals.countPerPage > 20) {
|
|
||||||
res.locals.countPerPage = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.locals.pagination = this.getPaginationParameters(req, res.locals.countPerPage);
|
|
||||||
res.locals.comments = await commentService.getReplies(res.locals.comment, res.locals.pagination);
|
|
||||||
|
|
||||||
const html = await commentService.renderTemplate('replyList', res.locals);
|
|
||||||
|
|
||||||
const replyList = `ul.dtp-reply-list[data-comment-id="${res.locals.comment._id}"]`;
|
|
||||||
displayList.addElement(replyList, 'beforeEnd', html);
|
|
||||||
|
|
||||||
const replyListContainer = `.dtp-reply-list-container[data-comment-id="${res.locals.comment._id}"]`;
|
|
||||||
displayList.removeAttribute(replyListContainer, 'hidden');
|
|
||||||
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to display comment replies', { error });
|
|
||||||
res.status(error.statusCode || 500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteComment (req, res) {
|
|
||||||
const { comment: commentService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('add-recipient');
|
|
||||||
|
|
||||||
await commentService.remove(res.locals.comment, 'removed');
|
|
||||||
|
|
||||||
let selector = `article[data-comment-id="${res.locals.comment._id}"] .comment-content`;
|
|
||||||
displayList.setTextContent(selector, 'Comment removed');
|
|
||||||
|
|
||||||
displayList.showNotification(
|
|
||||||
'Comment removed successfully',
|
|
||||||
'success',
|
|
||||||
'bottom-center',
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to remove comment', { error });
|
|
||||||
return res.status(error.statusCode || 500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
slug: 'comment',
|
|
||||||
name: 'comment',
|
|
||||||
create: async (dtp) => {
|
|
||||||
let controller = new CommentController(dtp);
|
|
||||||
return controller;
|
|
||||||
},
|
|
||||||
};
|
|
@ -0,0 +1,78 @@
|
|||||||
|
// email.js
|
||||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const DTP_COMPONENT_NAME = 'email';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
const { SiteController/*, SiteError*/ } = require('../../lib/site-lib');
|
||||||
|
|
||||||
|
class EmailController extends SiteController {
|
||||||
|
|
||||||
|
constructor (dtp) {
|
||||||
|
super(dtp, DTP_COMPONENT_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start ( ) {
|
||||||
|
const { jobQueue: jobQueueService, limiter: limiterService } = this.dtp.services;
|
||||||
|
|
||||||
|
this.emailJobQueue = jobQueueService.getJobQueue('email', {
|
||||||
|
attempts: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
this.dtp.app.use('/email', router);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/verify',
|
||||||
|
limiterService.create(limiterService.config.email.getEmailVerify),
|
||||||
|
this.getEmailVerify.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/opt-out',
|
||||||
|
limiterService.create(limiterService.config.email.getEmailOptOut),
|
||||||
|
this.getEmailOptOut.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmailOptOut (req, res, next) {
|
||||||
|
const { 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,
|
||||||
|
});
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmailVerify (req, res, next) {
|
||||||
|
const { email: emailService } = 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 });
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
slug: 'email',
|
||||||
|
name: 'email',
|
||||||
|
create: async (dtp) => {
|
||||||
|
let controller = new EmailController(dtp);
|
||||||
|
return controller;
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,64 @@
|
|||||||
|
// hive.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const DTP_COMPONENT_NAME = 'hive';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
const { SiteController } = require('../../lib/site-lib');
|
||||||
|
|
||||||
|
class HiveController extends SiteController {
|
||||||
|
|
||||||
|
constructor (dtp) {
|
||||||
|
super(dtp, DTP_COMPONENT_NAME);
|
||||||
|
this.services = [ ];
|
||||||
|
}
|
||||||
|
|
||||||
|
async start ( ) {
|
||||||
|
const router = express.Router();
|
||||||
|
this.dtp.app.use('/hive', router);
|
||||||
|
|
||||||
|
router.use(
|
||||||
|
async (req, res, next) => {
|
||||||
|
res.locals.currentView = 'hive';
|
||||||
|
res.locals.hiveView = 'home';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: H1V3 authentication before processing request (HTTP Bearer token)
|
||||||
|
*/
|
||||||
|
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.use('/kaleidoscope',await this.loadChild(path.join(__dirname, 'hive', 'kaleidoscope')));
|
||||||
|
this.services.push({ name: 'kaleidoscope', url: '/hive/kaleidoscope' });
|
||||||
|
|
||||||
|
router.get('/', this.getHiveRoot.bind(this));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHiveRoot (req, res) {
|
||||||
|
res.status(200).json({
|
||||||
|
component: DTP_COMPONENT_NAME,
|
||||||
|
host: this.dtp.pkg.name,
|
||||||
|
description: this.dtp.pkg.description,
|
||||||
|
version: this.dtp.pkg.version,
|
||||||
|
services: this.services,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
slug: 'hive',
|
||||||
|
name: 'hive',
|
||||||
|
create: async (dtp) => {
|
||||||
|
let controller = new HiveController(dtp);
|
||||||
|
return controller;
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,72 @@
|
|||||||
|
// hive/kaleidoscope.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const DTP_COMPONENT_NAME = 'hive:kaleidoscope';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
const { SiteController } = require('../../../lib/site-lib');
|
||||||
|
|
||||||
|
class HostController extends SiteController {
|
||||||
|
|
||||||
|
constructor (dtp) {
|
||||||
|
super(dtp, DTP_COMPONENT_NAME);
|
||||||
|
|
||||||
|
this.methods = [
|
||||||
|
{
|
||||||
|
name: 'postEvent',
|
||||||
|
url: '/kaleidoscope/event',
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async start ( ) {
|
||||||
|
const router = express.Router();
|
||||||
|
router.use(async (req, res, next) => {
|
||||||
|
res.locals.currentView = 'hive';
|
||||||
|
res.locals.hiveView = 'kaleidoscope';
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/core-node/connect', this.postCoreNodeConnect.bind(this));
|
||||||
|
router.post('/event', this.postEvent.bind(this));
|
||||||
|
|
||||||
|
router.get('/', this.getKaleidoscopeRoot.bind(this));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
async postCoreNodeConnect (req, res, next) {
|
||||||
|
const { coreNode: coreNodeService } = this.dtp.services;
|
||||||
|
try {
|
||||||
|
await coreNodeService.connect(req.body);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to create Core Node connection', { error });
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async postEvent (req, res) {
|
||||||
|
this.log.debug('kaleidoscope event received', { event: req.body.event });
|
||||||
|
this.emit('kaleidoscope:event', req, res);
|
||||||
|
res.status(200).json({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getKaleidoscopeRoot (req, res) {
|
||||||
|
res.status(200).json({
|
||||||
|
component: DTP_COMPONENT_NAME,
|
||||||
|
version: this.dtp.pkg.version,
|
||||||
|
services: this.services,
|
||||||
|
methods: this.methods,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (dtp) => {
|
||||||
|
let controller = new HostController(dtp);
|
||||||
|
return controller;
|
||||||
|
};
|
@ -1,104 +0,0 @@
|
|||||||
// newsletter.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const DTP_COMPONENT_NAME = 'newsletter';
|
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const multer = require('multer');
|
|
||||||
|
|
||||||
const { SiteController } = require('../../lib/site-lib');
|
|
||||||
|
|
||||||
class NewsletterController extends SiteController {
|
|
||||||
|
|
||||||
constructor (dtp) {
|
|
||||||
super(dtp, DTP_COMPONENT_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
async start ( ) {
|
|
||||||
const { dtp } = this;
|
|
||||||
const { limiter: limiterService } = dtp.services;
|
|
||||||
|
|
||||||
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}` });
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
dtp.app.use('/newsletter', router);
|
|
||||||
|
|
||||||
router.use(async (req, res, next) => {
|
|
||||||
res.locals.currentView = DTP_COMPONENT_NAME;
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.param('newsletterId', this.populateNewsletterId.bind(this));
|
|
||||||
|
|
||||||
router.post('/', upload.none(), this.postAddRecipient.bind(this));
|
|
||||||
|
|
||||||
router.get('/:newsletterId',
|
|
||||||
limiterService.create(limiterService.config.newsletter.getView),
|
|
||||||
this.getView.bind(this),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get('/',
|
|
||||||
limiterService.create(limiterService.config.newsletter.getIndex),
|
|
||||||
this.getIndex.bind(this),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async populateNewsletterId (req, res, next, newsletterId) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.newsletter = await newsletterService.getById(newsletterId);
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to populate newsletterId', { newsletterId, error });
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postAddRecipient (req, res) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
const displayList = this.createDisplayList('add-recipient');
|
|
||||||
await newsletterService.addRecipient(req.body.email);
|
|
||||||
displayList.showNotification(
|
|
||||||
'You have been added to the newsletter. Please check your email and verify your email address.',
|
|
||||||
'success',
|
|
||||||
'bottom-center',
|
|
||||||
10000,
|
|
||||||
);
|
|
||||||
res.status(200).json({ success: true, displayList });
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to update account settings', { error });
|
|
||||||
return res.status(error.statusCode || 500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getView (req, res) {
|
|
||||||
res.render('newsletter/view');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIndex (req, res, next) {
|
|
||||||
const { newsletter: newsletterService } = this.dtp.services;
|
|
||||||
try {
|
|
||||||
res.locals.pagination = this.getPaginationParameters(req, 20);
|
|
||||||
res.locals.newsletters = await newsletterService.getNewsletters(res.locals.pagination);
|
|
||||||
res.render('newsletter/index');
|
|
||||||
} catch (error) {
|
|
||||||
return next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
slug: 'newsletter',
|
|
||||||
name: 'newsletter',
|
|
||||||
create: async (dtp) => {
|
|
||||||
let controller = new NewsletterController(dtp);
|
|
||||||
return controller;
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
// category.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const CategorySchema = new Schema({
|
|
||||||
name: { type: String },
|
|
||||||
slug: { type: String, lowercase: true, required: true, index: 1 },
|
|
||||||
description: { type: String },
|
|
||||||
images: {
|
|
||||||
header: { type: Schema.ObjectId },
|
|
||||||
icon: { type: Schema.ObjectId },
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
articleCount: { type: Number, default: 0, required: true },
|
|
||||||
articleViewCount: { type: Number, default: 0, required: true },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = mongoose.model('Category', CategorySchema);
|
|
@ -0,0 +1,37 @@
|
|||||||
|
// core-node-request.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Used for authenticating responses received and gathering performance and use
|
||||||
|
* metrics for communications with Cores.
|
||||||
|
*
|
||||||
|
* When a request is created, an authentication token is generated and
|
||||||
|
* information about the request is stored. This also provides the request ID.
|
||||||
|
*
|
||||||
|
* When a resonse is received for a request, this record is fetched. The token
|
||||||
|
* claimed status and value are checked. Information about the response is
|
||||||
|
* recorded, and request execution time information is recorded.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CoreNodeRequestSchema = new Schema({
|
||||||
|
created: { type: Date, default: Date.now, required: true, index: 1 },
|
||||||
|
core: { type: Schema.ObjectId, required: true, ref: 'CoreNode' },
|
||||||
|
token: {
|
||||||
|
value: { type: String, required: true },
|
||||||
|
claimed: { type: Boolean, default: false, required: true },
|
||||||
|
},
|
||||||
|
url: { type: String },
|
||||||
|
response: {
|
||||||
|
received: { type: Date },
|
||||||
|
elapsed: { type: Number },
|
||||||
|
isError: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('CoreNodeRequest', CoreNodeRequestSchema);
|
@ -0,0 +1,18 @@
|
|||||||
|
// core-node.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const CoreNodeSchema = new Schema({
|
||||||
|
created: { type: Date, default: Date.now, required: true, index: 1 },
|
||||||
|
address: {
|
||||||
|
host: { type: String, required: true },
|
||||||
|
port: { type: Number, min: 1, max: 65535, required: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('CoreNode', CoreNodeSchema);
|
@ -0,0 +1,19 @@
|
|||||||
|
// email-log.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// All Rights Reserved
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const EmailLogSchema = new Schema({
|
||||||
|
created: { type: Date, default: Date.now, required: true, index: -1 },
|
||||||
|
from: { type: String, required: true, },
|
||||||
|
to: { type: String, required: true },
|
||||||
|
to_lc: { type: String, required: true, lowercase: true, index: 1 },
|
||||||
|
subject: { type: String },
|
||||||
|
messageId: { type: String },
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('EmailLog', EmailLogSchema);
|
@ -0,0 +1,18 @@
|
|||||||
|
// email-verify.js
|
||||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const EmailVerifySchema = new Schema({
|
||||||
|
created: { type: Date, default: Date.now, required: true, index: -1, expires: '30d' },
|
||||||
|
verified: { type: Date },
|
||||||
|
user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
|
||||||
|
token: { type: String, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('EmailVerify', EmailVerifySchema);
|
@ -1,21 +0,0 @@
|
|||||||
// newsletter-recipient.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const NewsletterRecipientSchema = new Schema({
|
|
||||||
created: { type: Date, default: Date.now, required: true, index: 1 },
|
|
||||||
address: { type: String, required: true },
|
|
||||||
address_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 },
|
|
||||||
flags: {
|
|
||||||
isVerified: { type: Boolean, default: false, required: true, index: 1 },
|
|
||||||
isOptIn: { type: Boolean, default: false, required: true, index: 1 },
|
|
||||||
isRejected: { type: Boolean, default: false, required: true, index: 1 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = mongoose.model('NewsletterRecipient', NewsletterRecipientSchema);
|
|
@ -1,31 +0,0 @@
|
|||||||
// newsletter.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const NEWSLETTER_STATUS_LIST = ['draft', 'published', 'archived'];
|
|
||||||
|
|
||||||
const NewsletterSchema = new Schema({
|
|
||||||
created: { type: Date, default: Date.now, required: true, index: -1 },
|
|
||||||
author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
|
|
||||||
title: { type: String, required: true },
|
|
||||||
summary: { type: String },
|
|
||||||
content: {
|
|
||||||
html: { type: String, required: true, select: false, },
|
|
||||||
text: { type: String, required: true, select: false, },
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
enum: NEWSLETTER_STATUS_LIST,
|
|
||||||
default: 'draft',
|
|
||||||
required: true,
|
|
||||||
index: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = mongoose.model('Newsletter', NewsletterSchema);
|
|
@ -1,29 +0,0 @@
|
|||||||
// page.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const PAGE_STATUS_LIST = ['draft','published','archived'];
|
|
||||||
|
|
||||||
const PageSchema = new Schema({
|
|
||||||
title: { type: String, required: true },
|
|
||||||
slug: { type: String, required: true, lowercase: true, unique: true },
|
|
||||||
image: {
|
|
||||||
header: { type: Schema.ObjectId, ref: 'Image' },
|
|
||||||
icon: { type: Schema.ObjectId, ref: 'Image' },
|
|
||||||
},
|
|
||||||
content: { type: String, required: true, select: false },
|
|
||||||
status: { type: String, enum: PAGE_STATUS_LIST, default: 'draft', index: true },
|
|
||||||
menu: {
|
|
||||||
icon: { type: String, required: true },
|
|
||||||
label: { type: String, required: true },
|
|
||||||
order: { type: Number, default: 0, required: true },
|
|
||||||
parent: { type: Schema.ObjectId, index: 1, ref: 'Page' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = mongoose.model('Page', PageSchema);
|
|
@ -1,36 +0,0 @@
|
|||||||
// post.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const {
|
|
||||||
ResourceStats,
|
|
||||||
ResourceStatsDefaults,
|
|
||||||
} = require(path.join(__dirname, 'lib', 'resource-stats.js'));
|
|
||||||
|
|
||||||
const POST_STATUS_LIST = ['draft','published','archived'];
|
|
||||||
|
|
||||||
const PostSchema = new Schema({
|
|
||||||
created: { type: Date, default: Date.now, required: true, index: -1 },
|
|
||||||
updated: { type: Date },
|
|
||||||
author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
|
|
||||||
image: { type: Schema.ObjectId, ref: 'Image' },
|
|
||||||
title: { type: String, required: true },
|
|
||||||
slug: { type: String, required: true, lowercase: true, unique: true },
|
|
||||||
summary: { type: String, required: true },
|
|
||||||
content: { type: String, required: true, select: false },
|
|
||||||
status: { type: String, enum: POST_STATUS_LIST, default: 'draft', index: true },
|
|
||||||
stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
|
|
||||||
flags: {
|
|
||||||
enableComments: { type: Boolean, default: true, index: true },
|
|
||||||
isFeatured: { type: Boolean, default: false, index: true },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = mongoose.model('Post', PostSchema);
|
|
@ -0,0 +1,97 @@
|
|||||||
|
// core-node.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const uuidv4 = require('uuid').v4;
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const fetch = require('node-fetch'); // jshint ignore:line
|
||||||
|
|
||||||
|
const CoreNode = mongoose.model('CoreNode');
|
||||||
|
const CoreNodeRequest = mongoose.model('CoreNodeRequest');
|
||||||
|
|
||||||
|
const { SiteService, SiteError } = require('../../lib/site-lib');
|
||||||
|
|
||||||
|
class CoreNodeService extends SiteService {
|
||||||
|
|
||||||
|
constructor (dtp) {
|
||||||
|
super(dtp, module.exports);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create (coreDefinition) {
|
||||||
|
const core = new CoreNode();
|
||||||
|
core.created = new Date();
|
||||||
|
|
||||||
|
core.address = { };
|
||||||
|
|
||||||
|
if (!coreDefinition.host) {
|
||||||
|
throw new SiteError(406, 'Must provide Core Node host address');
|
||||||
|
}
|
||||||
|
core.address.host = coreDefinition.host.trim();
|
||||||
|
|
||||||
|
if (!coreDefinition.port) {
|
||||||
|
throw new SiteError(406, 'Must provide Core Node TCP port number');
|
||||||
|
}
|
||||||
|
|
||||||
|
coreDefinition.port = parseInt(coreDefinition.port, 10);
|
||||||
|
if (coreDefinition.port < 1 || coreDefinition.port > 65535) {
|
||||||
|
throw new SiteError(406, 'Core Node port number out of range');
|
||||||
|
}
|
||||||
|
|
||||||
|
await core.save();
|
||||||
|
|
||||||
|
return core.toObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
async broadcast (request) {
|
||||||
|
const results = [ ];
|
||||||
|
await CoreNode
|
||||||
|
.find()
|
||||||
|
.cursor()
|
||||||
|
.eachAsync(async (core) => {
|
||||||
|
try {
|
||||||
|
const response = await this.sendRequest(core, request);
|
||||||
|
results.push({ coreId: core._id, request, response });
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to send Core Node request', { core: core._id, request: request.url, error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendRequest (core, request) {
|
||||||
|
const requestUrl = `https://${core.address.host}:${core.address.port}${request.url}`;
|
||||||
|
|
||||||
|
const req = new CoreNodeRequest();
|
||||||
|
req.created = new Date();
|
||||||
|
req.core = core._id;
|
||||||
|
req.token = {
|
||||||
|
value: uuidv4(),
|
||||||
|
claimed: false,
|
||||||
|
};
|
||||||
|
req.url = request.url;
|
||||||
|
await req.save();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
method: request.method,
|
||||||
|
body: request.body,
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
return { request: req.toObject(), response: json };
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to send Core Node request', { core: core._id, request: request.url, error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.toObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
slug: 'core-node',
|
||||||
|
name: 'coreNode',
|
||||||
|
create: (dtp) => { return new CoreNodeService(dtp); },
|
||||||
|
};
|
@ -1,123 +0,0 @@
|
|||||||
// newsletter.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const striptags = require('striptags');
|
|
||||||
|
|
||||||
const { SiteService } = require('../../lib/site-lib');
|
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const Newsletter = mongoose.model('Newsletter');
|
|
||||||
const NewsletterRecipient = mongoose.model('NewsletterRecipient');
|
|
||||||
|
|
||||||
class NewsletterService extends SiteService {
|
|
||||||
|
|
||||||
constructor (dtp) {
|
|
||||||
super(dtp, module.exports);
|
|
||||||
|
|
||||||
this.populateNewsletter = [
|
|
||||||
{
|
|
||||||
path: 'author',
|
|
||||||
select: '_id username username_lc displayName picture',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async create (author, newsletterDefinition) {
|
|
||||||
const NOW = new Date();
|
|
||||||
|
|
||||||
const newsletter = new Newsletter();
|
|
||||||
newsletter.created = NOW;
|
|
||||||
newsletter.author = author._id;
|
|
||||||
newsletter.title = striptags(newsletterDefinition.title.trim());
|
|
||||||
newsletter.summary = striptags(newsletterDefinition.summary.trim());
|
|
||||||
newsletter.content.html = newsletterDefinition['content.html'].trim();
|
|
||||||
newsletter.content.text = striptags(newsletterDefinition['content.text'].trim());
|
|
||||||
newsletter.status = 'draft';
|
|
||||||
|
|
||||||
await newsletter.save();
|
|
||||||
|
|
||||||
return newsletter.toObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
async update (newsletter, newsletterDefinition) {
|
|
||||||
const updateOp = { $set: { } };
|
|
||||||
|
|
||||||
if (newsletterDefinition.title) {
|
|
||||||
updateOp.$set.title = striptags(newsletterDefinition.title.trim());
|
|
||||||
}
|
|
||||||
if (newsletterDefinition.summary) {
|
|
||||||
updateOp.$set.summary = striptags(newsletterDefinition.summary.trim());
|
|
||||||
}
|
|
||||||
if (newsletterDefinition['content.html']) {
|
|
||||||
updateOp.$set['content.html'] = newsletterDefinition['content.html'].trim();
|
|
||||||
}
|
|
||||||
if (newsletterDefinition['content.text']) {
|
|
||||||
updateOp.$set['content.text'] = striptags(newsletterDefinition['content.text'].trim());
|
|
||||||
}
|
|
||||||
if (newsletterDefinition.status) {
|
|
||||||
updateOp.$set.status = striptags(newsletterDefinition.status.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(updateOp.$set).length === 0) {
|
|
||||||
return; // no update to perform
|
|
||||||
}
|
|
||||||
|
|
||||||
await Newsletter.updateOne(
|
|
||||||
{ _id: newsletter._id },
|
|
||||||
updateOp,
|
|
||||||
{ upsert: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNewsletters (pagination, status = ['published']) {
|
|
||||||
if (!Array.isArray(status)) {
|
|
||||||
status = [status];
|
|
||||||
}
|
|
||||||
const newsletters = await Newsletter
|
|
||||||
.find({ status: { $in: status } })
|
|
||||||
.sort({ created: -1 })
|
|
||||||
.skip(pagination.skip)
|
|
||||||
.limit(pagination.cpp)
|
|
||||||
.lean();
|
|
||||||
return newsletters;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getById (newsletterId) {
|
|
||||||
const newsletter = await Newsletter
|
|
||||||
.findById(newsletterId)
|
|
||||||
.select('+content.html +content.text')
|
|
||||||
.populate(this.populateNewsletter)
|
|
||||||
.lean();
|
|
||||||
return newsletter;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addRecipient (emailAddress) {
|
|
||||||
const { email: emailService } = this.dtp.services;
|
|
||||||
const NOW = new Date();
|
|
||||||
|
|
||||||
await emailService.checkEmailAddress(emailAddress);
|
|
||||||
|
|
||||||
const recipient = new NewsletterRecipient();
|
|
||||||
recipient.created = NOW;
|
|
||||||
recipient.address = striptags(emailAddress.trim());
|
|
||||||
recipient.address_lc = recipient.address.toLowerCase();
|
|
||||||
await recipient.save();
|
|
||||||
|
|
||||||
return recipient.toObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNewsletter (newsletter) {
|
|
||||||
this.log.info('deleting newsletter', { newsletterId: newsletter._id });
|
|
||||||
await Newsletter.deleteOne({ _id: newsletter._id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
slug: 'newsletter',
|
|
||||||
name: 'newsletter',
|
|
||||||
create: (dtp) => { return new NewsletterService(dtp); },
|
|
||||||
};
|
|
@ -0,0 +1,191 @@
|
|||||||
|
// oauth2.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const passport = require('passport');
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const uuidv4 = require('uuid').v4;
|
||||||
|
const oauth2orize = require('oauth2orize');
|
||||||
|
|
||||||
|
const { SiteService/*, SiteError*/ } = require('../../lib/site-lib');
|
||||||
|
|
||||||
|
class OAuth2Service extends SiteService {
|
||||||
|
|
||||||
|
constructor (dtp) {
|
||||||
|
super(dtp, module.exports);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start ( ) {
|
||||||
|
this.models = { };
|
||||||
|
|
||||||
|
/*
|
||||||
|
* OAuth2Client Model
|
||||||
|
*/
|
||||||
|
const ClientSchema = new Schema({
|
||||||
|
created: { type: Date, default: Date.now, required: true },
|
||||||
|
updated: { type: Date, default: Date.now, required: true },
|
||||||
|
secret: { type: String, required: true },
|
||||||
|
redirectURI: { type: String, required: true },
|
||||||
|
});
|
||||||
|
this.log.info('registering OAuth2Client model');
|
||||||
|
this.models.Client = mongoose.model('OAuth2Client', ClientSchema);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* OAuth2AuthorizationCode model
|
||||||
|
*/
|
||||||
|
const AuthorizationCodeSchema = new Schema({
|
||||||
|
code: { type: String, required: true, index: 1 },
|
||||||
|
clientId: { type: Schema.ObjectId, required: true, index: 1 },
|
||||||
|
redirectURI: { type: String, required: true },
|
||||||
|
user: { type: Schema.ObjectId, required: true, index: 1 },
|
||||||
|
scope: { type: [String], required: true },
|
||||||
|
});
|
||||||
|
this.log.info('registering OAuth2AuthorizationCode model');
|
||||||
|
this.models.AuthorizationCode = mongoose.model('OAuth2AuthorizationCode', AuthorizationCodeSchema);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* OAuth2AccessToken model
|
||||||
|
*/
|
||||||
|
const AccessTokenSchema = new Schema({
|
||||||
|
token: { type: String, required: true, unique: true, index: 1 },
|
||||||
|
user: { type: Schema.ObjectId, required: true, index: 1 },
|
||||||
|
clientId: { type: Schema.ObjectId, required: true, index: 1 },
|
||||||
|
scope: { type: [String], required: true },
|
||||||
|
});
|
||||||
|
this.log.info('registering OAuth2AccessToken model');
|
||||||
|
this.models.AccessToken = mongoose.model('OAuth2AccessToken', AccessTokenSchema);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create OAuth2 server instance
|
||||||
|
*/
|
||||||
|
const options = { };
|
||||||
|
this.log.info('creating OAuth2 server instance', { options });
|
||||||
|
this.server = oauth2orize.createServer(options);
|
||||||
|
this.server.grant(oauth2orize.grant.code(this.processGrant.bind(this)));
|
||||||
|
this.server.exchange(oauth2orize.exchange.code(this.processExchange.bind(this)));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Register client serialization callbacks
|
||||||
|
*/
|
||||||
|
this.log.info('registering OAuth2 client serialization routines');
|
||||||
|
this.server.serializeClient(this.serializeClient.bind(this));
|
||||||
|
this.server.deserializeClient(this.deserializeClient.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async serializeClient (client, done) {
|
||||||
|
return done(null, client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deserializeClient (clientId, done) {
|
||||||
|
try {
|
||||||
|
const client = await this.models.Client.findOne({ _id: clientId }).lean();
|
||||||
|
return done(null, client);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to deserialize OAuth2 client', { clientId, error });
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachRoutes (app) {
|
||||||
|
const { session: sessionService } = this.dtp.services;
|
||||||
|
|
||||||
|
const requireLogin = sessionService.authCheckMiddleware({ requireLogin: true });
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/dialog/authorize',
|
||||||
|
requireLogin,
|
||||||
|
this.server.authorize(this.processAuthorize.bind(this)),
|
||||||
|
this.renderAuthorizeDialog.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/dialog/authorize/decision',
|
||||||
|
requireLogin,
|
||||||
|
this.server.decision(),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/token',
|
||||||
|
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
|
||||||
|
this.server.token(),
|
||||||
|
this.server.errorHandler(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderAuthorizeDialog (req, res) {
|
||||||
|
res.locals.transactionID = req.oauth2.transactionID;
|
||||||
|
res.locals.client = req.oauth2.client;
|
||||||
|
res.render('oauth2/authorize-dialog');
|
||||||
|
}
|
||||||
|
|
||||||
|
async processAuthorize (clientID, redirectURI, done) {
|
||||||
|
try {
|
||||||
|
const client = await this.models.Clients.findOne({ clientID });
|
||||||
|
if (!client) {
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
if (client.redirectUri !== redirectURI) {
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
return done(null, client, client.redirectURI);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to process OAuth2 authorize', { error });
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processGrant (client, redirectURI, user, ares, done) {
|
||||||
|
try {
|
||||||
|
var code = uuidv4();
|
||||||
|
var ac = new this.models.AuthorizationCode({
|
||||||
|
code,
|
||||||
|
clientId: client.id,
|
||||||
|
redirectURI,
|
||||||
|
user: user.id,
|
||||||
|
scope: ares.scope,
|
||||||
|
});
|
||||||
|
await ac.save();
|
||||||
|
return done(null, code);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to process OAuth2 grant', { error });
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processExchange (client, code, redirectURI, done) {
|
||||||
|
try {
|
||||||
|
const ac = await this.models.AuthorizationCode.findOne({ code });
|
||||||
|
if (client.id !== ac.clientId) {
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
if (redirectURI !== ac.redirectUri) {
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = uuidv4();
|
||||||
|
var at = new this.models.AccessToken({
|
||||||
|
token,
|
||||||
|
user: ac.userId,
|
||||||
|
clientId: ac.clientId,
|
||||||
|
scope: ac.scope,
|
||||||
|
});
|
||||||
|
await at.save();
|
||||||
|
|
||||||
|
return done(null, token);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error('failed to process OAuth2 exchange', { error });
|
||||||
|
return done(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
slug: 'oauth2',
|
||||||
|
name: 'oauth2',
|
||||||
|
create: (dtp) => { return new OAuth2Service(dtp); },
|
||||||
|
};
|
@ -1,173 +0,0 @@
|
|||||||
// page.js
|
|
||||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const striptags = require('striptags');
|
|
||||||
const slug = require('slug');
|
|
||||||
|
|
||||||
const { SiteService } = require('../../lib/site-lib');
|
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const ObjectId = mongoose.Types.ObjectId;
|
|
||||||
|
|
||||||
const Page = mongoose.model('Page');
|
|
||||||
|
|
||||||
class PageService extends SiteService {
|
|
||||||
|
|
||||||
constructor (dtp) {
|
|
||||||
super(dtp, module.exports);
|
|
||||||
}
|
|
||||||
|
|
||||||
async menuMiddleware (req, res, next) {
|
|
||||||
try {
|
|
||||||
const pages = await Page
|
|
||||||
.find({ parent: { $exists: false } })
|
|
||||||
.select('slug menu')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
res.locals.mainMenu = pages
|
|
||||||
.filter((page) => !page.parent)
|
|
||||||
.map((page) => {
|
|
||||||
return {
|
|
||||||
url: `/page/${page.slug}`,
|
|
||||||
slug: page.slug,
|
|
||||||
icon: page.menu.icon,
|
|
||||||
label: page.menu.label,
|
|
||||||
order: page.menu.order,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.order < b.order;
|
|
||||||
});
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error('failed to build page menu', { error });
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async create (author, pageDefinition) {
|
|
||||||
const page = new Page();
|
|
||||||
page.title = striptags(pageDefinition.title.trim());
|
|
||||||
page.slug = this.createPageSlug(page._id, page.title);
|
|
||||||
page.content = pageDefinition.content.trim();
|
|
||||||
page.status = pageDefinition.status || 'draft';
|
|
||||||
|
|
||||||
page.menu = {
|
|
||||||
icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()),
|
|
||||||
label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))),
|
|
||||||
order: parseInt(pageDefinition.menuOrder || '0', 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
|
|
||||||
page.menu.parent = pageDefinition.parentPageId;
|
|
||||||
}
|
|
||||||
await page.save();
|
|
||||||
|
|
||||||
return page.toObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
async update (page, pageDefinition) {
|
|
||||||
const NOW = new Date();
|
|
||||||
const updateOp = {
|
|
||||||
$set: {
|
|
||||||
updated: NOW,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (pageDefinition.title) {
|
|
||||||
updateOp.$set.title = striptags(pageDefinition.title.trim());
|
|
||||||
}
|
|
||||||
if (pageDefinition.slug) {
|
|
||||||
let pageSlug = striptags(slug(pageDefinition.slug.trim())).split('-');
|
|
||||||
while (ObjectId.isValid(pageSlug[pageSlug.length - 1])) {
|
|
||||||
pageSlug.pop();
|
|
||||||
}
|
|
||||||
pageSlug = pageSlug.splice(0, 4);
|
|
||||||
pageSlug.push(page._id.toString());
|
|
||||||
updateOp.$set.slug = `${pageSlug.join('-')}`;
|
|
||||||
}
|
|
||||||
if (pageDefinition.summary) {
|
|
||||||
updateOp.$set.summary = striptags(pageDefinition.summary.trim());
|
|
||||||
}
|
|
||||||
if (pageDefinition.content) {
|
|
||||||
updateOp.$set.content = pageDefinition.content.trim();
|
|
||||||
}
|
|
||||||
if (pageDefinition.status) {
|
|
||||||
updateOp.$set.status = striptags(pageDefinition.status.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOp.$set.menu = {
|
|
||||||
icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()),
|
|
||||||
label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))),
|
|
||||||
order: parseInt(pageDefinition.menuOrder || '0', 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
|
|
||||||
updateOp.$set.menu.parent = pageDefinition.parentPageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Page.updateOne(
|
|
||||||
{ _id: page._id },
|
|
||||||
updateOp,
|
|
||||||
{ upsert: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPages (pagination, status = ['published']) {
|
|
||||||
if (!Array.isArray(status)) {
|
|
||||||
status = [status];
|
|
||||||
}
|
|
||||||
const pages = await Page
|
|
||||||
.find({ status: { $in: status } })
|
|
||||||
.sort({ created: -1 })
|
|
||||||
.skip(pagination.skip)
|
|
||||||
.limit(pagination.cpp)
|
|
||||||
.lean();
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getById (pageId) {
|
|
||||||
const page = await Page
|
|
||||||
.findById(pageId)
|
|
||||||
.select('+content')
|
|
||||||
.lean();
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBySlug (pageSlug) {
|
|
||||||
const slugParts = pageSlug.split('-');
|
|
||||||
const pageId = slugParts[slugParts.length - 1];
|
|
||||||
return this.getById(pageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAvailablePages (excludedPageIds) {
|
|
||||||
const search = { };
|
|
||||||
if (excludedPageIds) {
|
|
||||||
search._id = { $nin: excludedPageIds };
|
|
||||||
}
|
|
||||||
const pages = await Page.find(search).lean();
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deletePage (page) {
|
|
||||||
this.log.info('deleting page', { pageId: page._id });
|
|
||||||
await Page.deleteOne({ _id: page._id });
|
|
||||||
}
|
|
||||||
|
|
||||||
createPageSlug (pageId, pageTitle) {
|
|
||||||
if ((typeof pageTitle !== 'string') || (pageTitle.length < 1)) {
|
|
||||||
throw new Error('Invalid input for making a page slug');
|
|
||||||
}
|
|
||||||
const pageSlug = slug(pageTitle.trim().toLowerCase()).split('-').slice(0, 4).join('-');
|
|
||||||
return `${pageSlug}-${pageId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
slug: 'page',
|
|
||||||
name: 'page',
|
|
||||||
create: (dtp) => { return new PageService(dtp); },
|
|
||||||
};
|
|
@ -0,0 +1,61 @@
|
|||||||
|
// phone.js
|
||||||
|
// Copyright (C) 2022 DTP Technologies, LLC
|
||||||
|
// License: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const {
|
||||||
|
libphonenumber,
|
||||||
|
striptags,
|
||||||
|
SiteService,
|
||||||
|
SiteError,
|
||||||
|
} = require('../../lib/site-lib');
|
||||||
|
|
||||||
|
class PhoneService extends SiteService {
|
||||||
|
|
||||||
|
constructor (dtp) {
|
||||||
|
super(dtp, module.exports);
|
||||||
|
}
|
||||||
|
|
||||||
|
async processPhoneNumberInput (phoneNumberInput, country = 'US') {
|
||||||
|
const { parsePhoneNumber } = libphonenumber;
|
||||||
|
const phoneCheck = await parsePhoneNumber(striptags(phoneNumberInput.trim()), country);
|
||||||
|
|
||||||
|
if (!phoneCheck.isValid()) {
|
||||||
|
throw new SiteError(400, 'The phone number entered is not valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
// store everything this library provides about the new phone number
|
||||||
|
const phoneNumber = {
|
||||||
|
type: phoneCheck.getType(),
|
||||||
|
number: phoneCheck.number,
|
||||||
|
countryCallingCode: phoneCheck.countryCallingCode,
|
||||||
|
nationalNumber: phoneCheck.nationalNumber,
|
||||||
|
country: phoneCheck.country,
|
||||||
|
ext: phoneCheck.ext,
|
||||||
|
carrierCode: phoneCheck.carrierCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (phoneCheck.carrierCode) {
|
||||||
|
phoneNumber.carrierCode = phoneCheck.carrierCode;
|
||||||
|
}
|
||||||
|
if (phoneCheck.ext) {
|
||||||
|
phoneNumber.ext = phoneCheck.ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
phoneNumber.display = {
|
||||||
|
national: phoneCheck.formatNational(),
|
||||||
|
international: phoneCheck.formatInternational(),
|
||||||
|
uri: phoneCheck.getURI(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
slug: 'phone',
|
||||||
|
name: 'phone',
|
||||||
|
create: (dtp) => { return new PhoneService(dtp); },
|
||||||
|
};
|
@ -1,10 +1,8 @@
|
|||||||
.common-footer
|
.common-footer
|
||||||
|
p This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. You can #[a(href=`https://${site.domain}/email/opt-out?u=${recipient._id}&c=marketing`) opt out] at any time to stop receiving these emails.
|
||||||
p This email was sent to #{user.email} because you selected to receive emails from LocalRedAction.com. You can #[a(href=`https://localredaction.com/opt-out/${voter._id}/email`) opt out] at any time to stop receiving these emails.
|
//- p You can request to stop receiving these emails in writing at:
|
||||||
|
//- address
|
||||||
p You can request to stop receiving these emails in writing at:
|
//- div Digital Telepresence, LLC
|
||||||
address
|
//- div P.O. Box ########
|
||||||
div Local Red Action
|
//- div McKees Rocks, PA 15136
|
||||||
div P.O. Box ########
|
//- div USA
|
||||||
div McKees Rocks, PA 15136
|
|
||||||
div USA
|
|
@ -1 +1,2 @@
|
|||||||
.greeting Dear #{voter.name},
|
.common-title= emailTitle || `Greetings from ${site.name}!`
|
||||||
|
.common-slogan= site.description
|
@ -1,9 +1,10 @@
|
|||||||
|
|
|
||||||
| - - -
|
| - - -
|
||||||
| This email was sent to #{user.email} because you selected to receive emails from LocalRedAction.com. Visit #{`https://localredaction.com/opt-out/${voter._id}/email`} to opt out and stop receiving these emails.
|
| This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. Visit #{`https://${site.domain}/email/opt-out?u=${recipient._id}&c=marketing`} to opt out and stop receiving these emails.
|
||||||
|
|
|
|
||||||
| You can request to stop receiving these emails in writing at:
|
//- | You can request to stop receiving these emails in writing at:
|
||||||
|
|
//- |
|
||||||
| Local Red Action
|
//- | #{site.company}
|
||||||
| P.O. Box ########
|
//- | P.O. Box ########
|
||||||
| McKees Rocks, PA 15136
|
//- | McKees Rocks, PA 15136
|
||||||
| USA
|
//- | USA
|
@ -1 +1,2 @@
|
|||||||
| Dear #{voter.name},
|
| Dear #{recipient.displayName || recipient.username},
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
|||||||
|
extends ../layouts/html/system-message
|
||||||
|
block message-body
|
||||||
|
.content-message!= htmlMessage
|
@ -1,27 +1,4 @@
|
|||||||
doctype html
|
extends ../layouts/html/system-message
|
||||||
html(lang='en')
|
block content
|
||||||
head
|
p Welcome to #{site.name}! Please visit #[a(href=`https://${site.domain}/email/verify?t=${emailVerifyToken}`)= `https://${site.domain}/email/verify?t=${emailVerifyToken}`] to verify your email address and enable all features on your new account.
|
||||||
meta(charset='UTF-8')
|
p If you did not sign up for a new account at #{site.name}, please disregard this message.
|
||||||
meta(name='viewport', content='width=device-width, initial-scale=1.0')
|
|
||||||
meta(name='description', content= pageDescription || siteDescription)
|
|
||||||
|
|
||||||
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name
|
|
||||||
|
|
||||||
style(type="text/css").
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.greeting { font-size: 1.5em; margin-bottom: 16px; }
|
|
||||||
.message {}
|
|
||||||
|
|
||||||
body
|
|
||||||
|
|
||||||
include ../common/html/header
|
|
||||||
|
|
||||||
.message
|
|
||||||
p Welcome to #{service.name}! Please visit #[a(href=`https://localredaction.com/verify?t=${emailVerifyToken}`)= `https://localredaction.com/verify?t=${emailVerifyToken}`] to verify your email address.
|
|
||||||
|
|
||||||
p Thank you for supporting your local Republican committee and candidates!
|
|
||||||
|
|
||||||
include ../common/html/footer
|
|
@ -0,0 +1,106 @@
|
|||||||
|
doctype html
|
||||||
|
html(lang='en')
|
||||||
|
head
|
||||||
|
meta(charset='UTF-8')
|
||||||
|
meta(name='viewport', content='width=device-width, initial-scale=1.0')
|
||||||
|
meta(name='description', content= pageDescription || siteDescription)
|
||||||
|
|
||||||
|
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name
|
||||||
|
|
||||||
|
style(type="text/css").
|
||||||
|
html, body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 20px 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.section-muted {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
color: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-title {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto 8px auto;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-greeting {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto 20px auto;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-slogan {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-message {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-signature {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-footer {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-icon {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
padding: 6px 20px;
|
||||||
|
margin: 24px 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
outline: none;
|
||||||
|
background-color: #1093de;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
body
|
||||||
|
|
||||||
|
include ../library
|
||||||
|
|
||||||
|
section.section-muted
|
||||||
|
include ../../common/html/header
|
||||||
|
|
||||||
|
section
|
||||||
|
.common-greeting
|
||||||
|
div Dear #{recipient.displayName || recipient.username},
|
||||||
|
|
||||||
|
block message-body
|
||||||
|
.content-message
|
||||||
|
block content
|
||||||
|
|
||||||
|
.content-signature
|
||||||
|
p Thank you for your continued support!
|
||||||
|
p The #{site.name} team.
|
||||||
|
|
||||||
|
section.section-muted
|
||||||
|
include ../../common/html/footer
|
@ -0,0 +1,4 @@
|
|||||||
|
-
|
||||||
|
function formatCount (count) {
|
||||||
|
return numeral(count).format((count > 1000) ? '0,0.0a' : '0,0');
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
include ../library
|
||||||
|
include ../../common/text/header
|
||||||
|
|
|
||||||
|
block content
|
||||||
|
|
|
||||||
|
| Thank you for your continued support!
|
||||||
|
|
|
||||||
|
| The #{site.name} team.
|
||||||
|
|
|
||||||
|
include ../../common/text/footer
|
@ -0,0 +1,5 @@
|
|||||||
|
extends ../layouts/text/system-message
|
||||||
|
block content
|
||||||
|
|
|
||||||
|
| #{textMessage}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
|||||||
include ../common/text/header
|
extends ../layouts/text/system-message
|
||||||
|
|
block content
|
||||||
| Welcome to #{service.name}! Please visit #[a(href=`https://localredaction.com/verify?t=${emailVerifyToken}`)= `https://localredaction.com/verify?t=${emailVerifyToken}`] to verify your email address.
|
|
|
||||||
|
|
| Welcome to #{site.name}! Please visit #{`https://${site.domain}/email/verify?t=${emailVerifyToken}`} to verify your email address and enable all features on your new account.
|
||||||
| Thank you for supporting your local Republican committee and candidates!
|
|
|
||||||
|
|
| If you did not sign up for a new account at #{site.name}, please disregard this message.
|
||||||
include ../common/text/footer
|
|
|
@ -1,17 +0,0 @@
|
|||||||
extends ../layouts/main
|
|
||||||
block content
|
|
||||||
|
|
||||||
- var formAction = category ? `/admin/category/${category._id}` : '/admin/category';
|
|
||||||
|
|
||||||
pre= JSON.stringify(category, null, 2)
|
|
||||||
|
|
||||||
form(method="POST", action= formAction).uk-form
|
|
||||||
.uk-margin
|
|
||||||
label(for="name").uk-form-label Category Name
|
|
||||||
input(id="name", name="name", type="text", placeholder="Enter category name", value= category ? category.name : undefined).uk-input
|
|
||||||
|
|
||||||
.uk-margin
|
|
||||||
label(for="description").uk-form-label Description
|
|
||||||
textarea(id="description", name="description", rows="3", placeholder="Enter category description").uk-textarea= category ? category.description : undefined
|
|
||||||
|
|
||||||
button(type="submit").uk-button.uk-button-primary= category ? 'Update Category' : 'Create Category'
|
|
@ -1,21 +0,0 @@
|
|||||||
extends ../layouts/main
|
|
||||||
block content
|
|
||||||
|
|
||||||
.uk-margin
|
|
||||||
div(uk-grid).uk-flex-middle
|
|
||||||
.uk-width-expand
|
|
||||||
h2 Category Manager
|
|
||||||
.uk-width-auto
|
|
||||||
a(href="/admin/category/create").uk-button.uk-button-primary
|
|
||||||
span
|
|
||||||
i.fas.fa-plus
|
|
||||||
span.uk-margin-small-left Add category
|
|
||||||
|
|
||||||
.uk-margin
|
|
||||||
if Array.isArray(categories) && (categories.length > 0)
|
|
||||||
uk.uk-list
|
|
||||||
each category in categories
|
|
||||||
li
|
|
||||||
a(href=`/admin/category/${category._id}`)= category.name
|
|
||||||
else
|
|
||||||
h4 There are no categories.
|
|
@ -0,0 +1,18 @@
|
|||||||
|
extends ../layouts/main
|
||||||
|
block content
|
||||||
|
|
||||||
|
form(method="POST", action="/admin/core-node/connect").uk-form
|
||||||
|
.uk-card.uk-card-default.uk-card-small
|
||||||
|
.uk-card-header
|
||||||
|
h1.uk-card-title Connect to New Core
|
||||||
|
|
||||||
|
.uk-card-body
|
||||||
|
.uk-margin
|
||||||
|
label(for="host").uk-form-label Address
|
||||||
|
input(id="host", name="host", placeholder="Enter host name or address", required).uk-input
|
||||||
|
.uk-margin
|
||||||
|
label(for="port").uk-form-label Port Number
|
||||||
|
input(id="port", name="port", min="1", max="65535", step="1", value="4200", required).uk-input
|
||||||
|
|
||||||
|
.uk-card-footer
|
||||||
|
button(type="submit").uk-button.uk-button-primary Send Request
|
@ -0,0 +1,14 @@
|
|||||||
|
extends ../layouts/main
|
||||||
|
block content
|
||||||
|
|
||||||
|
h1 Core Nodes
|
||||||
|
a(href="/admin/core-node/connect").uk-button.uk-button-primary Connect Core
|
||||||
|
|
||||||
|
p You can register with one or more Core nodes to exchange information with those nodes.
|
||||||
|
|
||||||
|
if Array.isArray(coreNodes) && (coreNodes.length > 0)
|
||||||
|
ul.uk-list
|
||||||
|
each node in coreNodes
|
||||||
|
pre= JSON.stringify(node, null, 2)
|
||||||
|
else
|
||||||
|
p There are no registered core nodes.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue