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
|
||||
|
||||
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
|
||||
div Local Red Action
|
||||
div P.O. Box ########
|
||||
div McKees Rocks, PA 15136
|
||||
div USA
|
||||
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 You can request to stop receiving these emails in writing at:
|
||||
//- address
|
||||
//- div Digital Telepresence, LLC
|
||||
//- div P.O. Box ########
|
||||
//- 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:
|
||||
|
|
||||
| Local Red Action
|
||||
| P.O. Box ########
|
||||
| McKees Rocks, PA 15136
|
||||
| USA
|
||||
//- | You can request to stop receiving these emails in writing at:
|
||||
//- |
|
||||
//- | #{site.company}
|
||||
//- | P.O. Box ########
|
||||
//- | McKees Rocks, PA 15136
|
||||
//- | 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
|
||||
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 {
|
||||
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
|
||||
extends ../layouts/html/system-message
|
||||
block content
|
||||
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.
|
||||
p If you did not sign up for a new account at #{site.name}, please disregard this message.
|
@ -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
|
||||
|
|
||||
| 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.
|
||||
|
|
||||
| Thank you for supporting your local Republican committee and candidates!
|
||||
|
|
||||
include ../common/text/footer
|
||||
extends ../layouts/text/system-message
|
||||
block content
|
||||
|
|
||||
| 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.
|
||||
|
|
||||
| If you did not sign up for a new account at #{site.name}, please disregard this message.
|
||||
|
|
@ -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