forked from digital-telepresence/dtp-sites
Rob Colbert
2 years ago
234 changed files with 2830 additions and 3290 deletions
@ -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 changed in this diff
Loading…
Reference in new issue