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 |
.common-footer |
||||
|
p This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. You can #[a(href=`https://${site.domain}/email/opt-out?u=${recipient._id}&c=marketing`) opt out] at any time to stop receiving these emails. |
||||
p This email was sent to #{user.email} because you selected to receive emails from LocalRedAction.com. You can #[a(href=`https://localredaction.com/opt-out/${voter._id}/email`) opt out] at any time to stop receiving these emails. |
//- p You can request to stop receiving these emails in writing at: |
||||
|
//- address |
||||
p You can request to stop receiving these emails in writing at: |
//- div Digital Telepresence, LLC |
||||
address |
//- div P.O. Box ######## |
||||
div Local Red Action |
//- div McKees Rocks, PA 15136 |
||||
div P.O. Box ######## |
//- div USA |
||||
div McKees Rocks, PA 15136 |
|
||||
div USA |
|
@ -1 +1,2 @@ |
|||||
.greeting Dear #{voter.name}, |
.common-title= emailTitle || `Greetings from ${site.name}!` |
||||
|
.common-slogan= site.description |
@ -1,9 +1,10 @@ |
|||||
|
| |
||||
| - - - |
| - - - |
||||
| This email was sent to #{user.email} because you selected to receive emails from LocalRedAction.com. Visit #{`https://localredaction.com/opt-out/${voter._id}/email`} to opt out and stop receiving these emails. |
| This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. Visit #{`https://${site.domain}/email/opt-out?u=${recipient._id}&c=marketing`} to opt out and stop receiving these emails. |
||||
| |
| |
||||
| You can request to stop receiving these emails in writing at: |
//- | You can request to stop receiving these emails in writing at: |
||||
| |
//- | |
||||
| Local Red Action |
//- | #{site.company} |
||||
| P.O. Box ######## |
//- | P.O. Box ######## |
||||
| McKees Rocks, PA 15136 |
//- | McKees Rocks, PA 15136 |
||||
| USA |
//- | USA |
@ -1 +1,2 @@ |
|||||
| Dear #{voter.name}, |
| Dear #{recipient.displayName || recipient.username}, |
||||
|
| |
@ -0,0 +1,3 @@ |
|||||
|
extends ../layouts/html/system-message |
||||
|
block message-body |
||||
|
.content-message!= htmlMessage |
@ -1,27 +1,4 @@ |
|||||
doctype html |
extends ../layouts/html/system-message |
||||
html(lang='en') |
block content |
||||
head |
p Welcome to #{site.name}! Please visit #[a(href=`https://${site.domain}/email/verify?t=${emailVerifyToken}`)= `https://${site.domain}/email/verify?t=${emailVerifyToken}`] to verify your email address and enable all features on your new account. |
||||
meta(charset='UTF-8') |
p If you did not sign up for a new account at #{site.name}, please disregard this message. |
||||
meta(name='viewport', content='width=device-width, initial-scale=1.0') |
|
||||
meta(name='description', content= pageDescription || siteDescription) |
|
||||
|
|
||||
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name |
|
||||
|
|
||||
style(type="text/css"). |
|
||||
html, body { |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
} |
|
||||
.greeting { font-size: 1.5em; margin-bottom: 16px; } |
|
||||
.message {} |
|
||||
|
|
||||
body |
|
||||
|
|
||||
include ../common/html/header |
|
||||
|
|
||||
.message |
|
||||
p Welcome to #{service.name}! Please visit #[a(href=`https://localredaction.com/verify?t=${emailVerifyToken}`)= `https://localredaction.com/verify?t=${emailVerifyToken}`] to verify your email address. |
|
||||
|
|
||||
p Thank you for supporting your local Republican committee and candidates! |
|
||||
|
|
||||
include ../common/html/footer |
|
@ -0,0 +1,106 @@ |
|||||
|
doctype html |
||||
|
html(lang='en') |
||||
|
head |
||||
|
meta(charset='UTF-8') |
||||
|
meta(name='viewport', content='width=device-width, initial-scale=1.0') |
||||
|
meta(name='description', content= pageDescription || siteDescription) |
||||
|
|
||||
|
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name |
||||
|
|
||||
|
style(type="text/css"). |
||||
|
html, body { |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
background-color: #ffffff; |
||||
|
color: #1a1a1a; |
||||
|
} |
||||
|
|
||||
|
section { |
||||
|
padding: 20px 0; |
||||
|
background-color: #ffffff; |
||||
|
color: #1a1a1a; |
||||
|
} |
||||
|
|
||||
|
section.section-muted { |
||||
|
background-color: #f8f8f8; |
||||
|
color: #2a2a2a; |
||||
|
} |
||||
|
|
||||
|
.common-title { |
||||
|
max-width: 640px; |
||||
|
margin: 0 auto 8px auto; |
||||
|
font-size: 1.5em; |
||||
|
} |
||||
|
|
||||
|
.common-greeting { |
||||
|
max-width: 640px; |
||||
|
margin: 0 auto 20px auto; |
||||
|
font-size: 1.1em; |
||||
|
} |
||||
|
|
||||
|
.common-slogan { |
||||
|
max-width: 640px; |
||||
|
margin: 0 auto; |
||||
|
font-size: 1.1em; |
||||
|
font-style: italic; |
||||
|
} |
||||
|
|
||||
|
.content-message { |
||||
|
max-width: 640px; |
||||
|
margin: 0 auto; |
||||
|
background: white; |
||||
|
color: black; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.content-signature { |
||||
|
max-width: 640px; |
||||
|
margin: 0 auto; |
||||
|
background: white; |
||||
|
color: black; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.common-footer { |
||||
|
max-width: 640px; |
||||
|
margin: 0 auto; |
||||
|
font-size: 10px; |
||||
|
} |
||||
|
|
||||
|
.channel-icon { |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
.action-button { |
||||
|
padding: 6px 20px; |
||||
|
margin: 24px 0; |
||||
|
border: none; |
||||
|
border-radius: 20px; |
||||
|
outline: none; |
||||
|
background-color: #1093de; |
||||
|
color: #ffffff; |
||||
|
font-size: 16px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
body |
||||
|
|
||||
|
include ../library |
||||
|
|
||||
|
section.section-muted |
||||
|
include ../../common/html/header |
||||
|
|
||||
|
section |
||||
|
.common-greeting |
||||
|
div Dear #{recipient.displayName || recipient.username}, |
||||
|
|
||||
|
block message-body |
||||
|
.content-message |
||||
|
block content |
||||
|
|
||||
|
.content-signature |
||||
|
p Thank you for your continued support! |
||||
|
p The #{site.name} team. |
||||
|
|
||||
|
section.section-muted |
||||
|
include ../../common/html/footer |
@ -0,0 +1,4 @@ |
|||||
|
- |
||||
|
function formatCount (count) { |
||||
|
return numeral(count).format((count > 1000) ? '0,0.0a' : '0,0'); |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
include ../library |
||||
|
include ../../common/text/header |
||||
|
| |
||||
|
block content |
||||
|
| |
||||
|
| Thank you for your continued support! |
||||
|
| |
||||
|
| The #{site.name} team. |
||||
|
| |
||||
|
include ../../common/text/footer |
@ -0,0 +1,5 @@ |
|||||
|
extends ../layouts/text/system-message |
||||
|
block content |
||||
|
| |
||||
|
| #{textMessage} |
||||
|
| |
@ -1,7 +1,7 @@ |
|||||
include ../common/text/header |
extends ../layouts/text/system-message |
||||
| |
block content |
||||
| Welcome to #{service.name}! Please visit #[a(href=`https://localredaction.com/verify?t=${emailVerifyToken}`)= `https://localredaction.com/verify?t=${emailVerifyToken}`] to verify your email address. |
| |
||||
| |
| Welcome to #{site.name}! Please visit #{`https://${site.domain}/email/verify?t=${emailVerifyToken}`} to verify your email address and enable all features on your new account. |
||||
| Thank you for supporting your local Republican committee and candidates! |
| |
||||
| |
| If you did not sign up for a new account at #{site.name}, please disregard this message. |
||||
include ../common/text/footer |
| |
@ -1,17 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
- var formAction = category ? `/admin/category/${category._id}` : '/admin/category'; |
|
||||
|
|
||||
pre= JSON.stringify(category, null, 2) |
|
||||
|
|
||||
form(method="POST", action= formAction).uk-form |
|
||||
.uk-margin |
|
||||
label(for="name").uk-form-label Category Name |
|
||||
input(id="name", name="name", type="text", placeholder="Enter category name", value= category ? category.name : undefined).uk-input |
|
||||
|
|
||||
.uk-margin |
|
||||
label(for="description").uk-form-label Description |
|
||||
textarea(id="description", name="description", rows="3", placeholder="Enter category description").uk-textarea= category ? category.description : undefined |
|
||||
|
|
||||
button(type="submit").uk-button.uk-button-primary= category ? 'Update Category' : 'Create Category' |
|
@ -1,21 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
.uk-margin |
|
||||
div(uk-grid).uk-flex-middle |
|
||||
.uk-width-expand |
|
||||
h2 Category Manager |
|
||||
.uk-width-auto |
|
||||
a(href="/admin/category/create").uk-button.uk-button-primary |
|
||||
span |
|
||||
i.fas.fa-plus |
|
||||
span.uk-margin-small-left Add category |
|
||||
|
|
||||
.uk-margin |
|
||||
if Array.isArray(categories) && (categories.length > 0) |
|
||||
uk.uk-list |
|
||||
each category in categories |
|
||||
li |
|
||||
a(href=`/admin/category/${category._id}`)= category.name |
|
||||
else |
|
||||
h4 There are no categories. |
|
@ -0,0 +1,18 @@ |
|||||
|
extends ../layouts/main |
||||
|
block content |
||||
|
|
||||
|
form(method="POST", action="/admin/core-node/connect").uk-form |
||||
|
.uk-card.uk-card-default.uk-card-small |
||||
|
.uk-card-header |
||||
|
h1.uk-card-title Connect to New Core |
||||
|
|
||||
|
.uk-card-body |
||||
|
.uk-margin |
||||
|
label(for="host").uk-form-label Address |
||||
|
input(id="host", name="host", placeholder="Enter host name or address", required).uk-input |
||||
|
.uk-margin |
||||
|
label(for="port").uk-form-label Port Number |
||||
|
input(id="port", name="port", min="1", max="65535", step="1", value="4200", required).uk-input |
||||
|
|
||||
|
.uk-card-footer |
||||
|
button(type="submit").uk-button.uk-button-primary Send Request |
@ -0,0 +1,14 @@ |
|||||
|
extends ../layouts/main |
||||
|
block content |
||||
|
|
||||
|
h1 Core Nodes |
||||
|
a(href="/admin/core-node/connect").uk-button.uk-button-primary Connect Core |
||||
|
|
||||
|
p You can register with one or more Core nodes to exchange information with those nodes. |
||||
|
|
||||
|
if Array.isArray(coreNodes) && (coreNodes.length > 0) |
||||
|
ul.uk-list |
||||
|
each node in coreNodes |
||||
|
pre= JSON.stringify(node, null, 2) |
||||
|
else |
||||
|
p There are no registered core nodes. |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue