Browse Source

Merge branch 'develop' of git.digitaltelepresence.com:digital-telepresence/dtp-base into develop

userService-patch
Rob Colbert 2 years ago
parent
commit
0dd25e5176
  1. 14
      .env.default
  2. 10
      .vscode/launch.json
  3. 4
      LICENSE
  4. 2
      NOTES.md
  5. 6
      app/controllers/admin.js
  6. 2
      app/controllers/admin/content-report.js
  7. 60
      app/controllers/admin/core-node.js
  8. 2
      app/controllers/admin/host.js
  9. 56
      app/controllers/admin/job-queue.js
  10. 2
      app/controllers/admin/log.js
  11. 123
      app/controllers/admin/newsletter.js
  12. 135
      app/controllers/admin/page.js
  13. 152
      app/controllers/admin/post.js
  14. 2
      app/controllers/admin/settings.js
  15. 2
      app/controllers/admin/user.js
  16. 2
      app/controllers/auth.js
  17. 158
      app/controllers/comment.js
  18. 78
      app/controllers/email.js
  19. 64
      app/controllers/hive.js
  20. 72
      app/controllers/hive/kaleidoscope.js
  21. 17
      app/controllers/home.js
  22. 2
      app/controllers/image.js
  23. 4
      app/controllers/manifest.js
  24. 104
      app/controllers/newsletter.js
  25. 70
      app/controllers/user.js
  26. 2
      app/controllers/welcome.js
  27. 25
      app/models/category.js
  28. 2
      app/models/chat-message.js
  29. 2
      app/models/comment.js
  30. 2
      app/models/connect-token.js
  31. 2
      app/models/content-report.js
  32. 2
      app/models/content-vote.js
  33. 37
      app/models/core-node-request.js
  34. 18
      app/models/core-node.js
  35. 2
      app/models/csrf-token.js
  36. 2
      app/models/email-blacklist.js
  37. 2
      app/models/email-body.js
  38. 19
      app/models/email-log.js
  39. 18
      app/models/email-verify.js
  40. 2
      app/models/email.js
  41. 2
      app/models/image.js
  42. 2
      app/models/lib/geo-types.js
  43. 2
      app/models/lib/resource-stats.js
  44. 2
      app/models/log.js
  45. 2
      app/models/net-host-stats.js
  46. 2
      app/models/net-host.js
  47. 21
      app/models/newsletter-recipient.js
  48. 31
      app/models/newsletter.js
  49. 2
      app/models/otp-account.js
  50. 29
      app/models/page.js
  51. 36
      app/models/post.js
  52. 2
      app/models/resource-view.js
  53. 2
      app/models/resource-visit.js
  54. 2
      app/models/user-block.js
  55. 2
      app/models/user-notification.js
  56. 16
      app/models/user.js
  57. 2
      app/services/cache.js
  58. 2
      app/services/chat.js
  59. 2
      app/services/comment.js
  60. 2
      app/services/content-report.js
  61. 2
      app/services/content-vote.js
  62. 97
      app/services/core-node.js
  63. 2
      app/services/crypto.js
  64. 2
      app/services/csrf-token.js
  65. 9
      app/services/display-engine.js
  66. 86
      app/services/email.js
  67. 2
      app/services/host-cache.js
  68. 2
      app/services/image.js
  69. 3
      app/services/job-queue.js
  70. 2
      app/services/limiter.js
  71. 2
      app/services/log.js
  72. 2
      app/services/markdown.js
  73. 2
      app/services/media.js
  74. 2
      app/services/minio.js
  75. 123
      app/services/newsletter.js
  76. 191
      app/services/oauth2.js
  77. 10
      app/services/otp-auth.js
  78. 173
      app/services/page.js
  79. 61
      app/services/phone.js
  80. 2
      app/services/resource.js
  81. 2
      app/services/session.js
  82. 2
      app/services/sms.js
  83. 2
      app/services/user-notification.js
  84. 132
      app/services/user.js
  85. 16
      app/templates/common/html/footer.pug
  86. 3
      app/templates/common/html/header.pug
  87. 15
      app/templates/common/text/footer.pug
  88. 3
      app/templates/common/text/header.pug
  89. 3
      app/templates/html/user-email.pug
  90. 31
      app/templates/html/welcome.pug
  91. 106
      app/templates/layouts/html/system-message.pug
  92. 4
      app/templates/layouts/library.pug
  93. 10
      app/templates/layouts/text/system-message.pug
  94. 5
      app/templates/text/user-email.pug
  95. 14
      app/templates/text/welcome.pug
  96. 17
      app/views/admin/category/editor.pug
  97. 21
      app/views/admin/category/index.pug
  98. 23
      app/views/admin/components/menu.pug
  99. 18
      app/views/admin/core-node/connect.pug
  100. 14
      app/views/admin/core-node/index.pug

14
.env.default

@ -22,7 +22,7 @@ MAILGUN_DOMAIN=
#
MONGODB_HOST=localhost:27017
MONGODB_DATABASE=dtp-sites
MONGODB_DATABASE=dtp-webapp
#
# Redis configuration
@ -39,10 +39,10 @@ REDIS_PASSWORD=
MINIO_ENDPOINT=localhost
MINIO_PORT=9000
MINIO_USE_SSL=disabled
MINIO_ACCESS_KEY=dtp-sites
MINIO_ACCESS_KEY=dtp-webapp
MINIO_SECRET_KEY=
MINIO_IMAGE_BUCKET=site-images
MINIO_VIDEO_BUCKET=site-videos
MINIO_IMAGE_BUCKET=webapp-images
MINIO_VIDEO_BUCKET=webapp-videos
#
# ExpressJS/HTTP configuration
@ -60,9 +60,9 @@ DTP_LOG_CONSOLE=enabled
DTP_LOG_MONGODB=enabled
DTP_LOG_FILE=enabled
DTP_LOG_FILE_PATH=/tmp/dtp-sites/logs
DTP_LOG_FILE_NAME_APP=justjoeradio-app.log
DTP_LOG_FILE_NAME_HTTP=justjoeradio-access.log
DTP_LOG_FILE_PATH=/tmp/dtp-webapp/logs
DTP_LOG_FILE_NAME_APP=webapp-app.log
DTP_LOG_FILE_NAME_HTTP=webapp-access.log
DTP_LOG_DEBUG=enabled
DTP_LOG_INFO=enabled

10
.vscode/launch.json

@ -7,11 +7,11 @@
{
"type": "pwa-node",
"request": "launch",
"name": "web:dtp-sites",
"name": "web:dtp-webapp",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder:dtp-justjoeradio}/dtp-sites.js",
"program": "${workspaceFolder:dtp-sites}/dtp-sites.js",
"console": "integratedTerminal",
"env": {
"HTTP_BIND_PORT": "3333"
@ -20,13 +20,13 @@
{
"type": "pwa-node",
"request": "launch",
"name": "cli:dtp-sites",
"name": "cli:dtp-webapp",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder:dtp-justjoeradio}/dtp-sites-cli.js",
"program": "${workspaceFolder:dtp-sites}/dtp-sites-cli.js",
"console": "integratedTerminal",
"args": ["--action=reset-indexes", "all"]
}
]
}
}

4
LICENSE

@ -1,4 +1,4 @@
Copyright 2021 Digital Telepresence, LLC
Copyright 2022 DTP Technologies, LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -10,4 +10,4 @@ Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.

2
NOTES.md

@ -2,8 +2,6 @@
- Password
- Light mode
Want to tune in live
Want chat

6
app/controllers/admin.js

@ -1,5 +1,5 @@
// admin.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -45,12 +45,10 @@ class AdminController extends SiteController {
);
router.use('/content-report',await this.loadChild(path.join(__dirname, 'admin', 'content-report')));
router.use('/core-node',await this.loadChild(path.join(__dirname, 'admin', 'core-node')));
router.use('/host',await this.loadChild(path.join(__dirname, 'admin', 'host')));
router.use('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));
router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));
router.use('/page', await this.loadChild(path.join(__dirname, 'admin', 'page')));
router.use('/post', await this.loadChild(path.join(__dirname, 'admin', 'post')));
router.use('/settings', await this.loadChild(path.join(__dirname, 'admin', 'settings')));
router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user')));

2
app/controllers/admin/content-report.js

@ -1,5 +1,5 @@
// admin/content-report.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

60
app/controllers/admin/core-node.js

@ -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;
};

2
app/controllers/admin/host.js

@ -1,5 +1,5 @@
// admin/host.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

56
app/controllers/admin/job-queue.js

@ -1,13 +1,13 @@
// admin/job-queue.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
// Copyright (C) 2022 DTP Technologies, LLC
// All Rights Reserved
'use strict';
const DTP_COMPONENT_NAME = 'admin:job-queue';
const express = require('express');
const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class JobQueueController extends SiteController {
@ -24,7 +24,11 @@ class JobQueueController extends SiteController {
});
router.param('jobQueueName', this.populateJobQueueName.bind(this));
router.param('jobId', this.populateJob.bind(this));
router.post('/:jobQueueName/:jobId/action', this.postJobAction.bind(this));
router.get('/:jobQueueName/:jobId', this.getJobView.bind(this));
router.get('/:jobQueueName', this.getJobQueueView.bind(this));
router.get('/', this.getHomeView.bind(this));
@ -36,6 +40,9 @@ class JobQueueController extends SiteController {
try {
res.locals.queueName = jobQueueName;
res.locals.queue = await jobQueueService.getJobQueue(jobQueueName);
if (!res.locals.queue) {
throw new SiteError(404, 'Job queue not found');
}
return next();
} catch (error) {
this.log.error('failed to populate job queue', { jobQueueName, error });
@ -43,6 +50,46 @@ class JobQueueController extends SiteController {
}
}
async populateJob (req, res, next, jobId) {
try {
res.locals.job = await res.locals.queue.getJob(jobId);
if (!res.locals.job) {
throw new SiteError(404, 'Job not found');
}
return next();
} catch (error) {
this.log.error('failed to populate job', { jobId, error });
return next(error);
}
}
async postJobAction (req, res) {
try {
await res.locals.job[req.body.action]();
res.status(200).json({ success: true });
} catch (error) {
this.log.error('failed to execute job action', {
jobId: res.locals.job.id,
action: req.body.action,
error,
});
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async getJobView (req, res, next) {
try {
res.locals.jobLogs = await res.locals.queue.getJobLogs(res.locals.job.id);
res.render('admin/job-queue/job-view');
} catch (error) {
this.log.error('failed to render job view', { error });
return next(error);
}
}
async getJobQueueView (req, res, next) {
try {
res.locals.jobCounts = await res.locals.queue.getJobCounts();
@ -62,7 +109,8 @@ class JobQueueController extends SiteController {
async getHomeView (req, res, next) {
const { jobQueue: jobQueueService } = this.dtp.services;
try {
res.locals.queues = await jobQueueService.discoverJobQueues('soapy:*:id');
const prefix = process.env.REDIS_KEY_PREFIX || 'dtp';
res.locals.queues = await jobQueueService.discoverJobQueues(`${prefix}:*:id`);
res.render('admin/job-queue/index');
} catch (error) {
this.log.error('failed to populate job queues view', { error });

2
app/controllers/admin/log.js

@ -1,5 +1,5 @@
// admin/log.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

123
app/controllers/admin/newsletter.js

@ -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;
};

135
app/controllers/admin/page.js

@ -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;
};

152
app/controllers/admin/post.js

@ -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;
};

2
app/controllers/admin/settings.js

@ -1,5 +1,5 @@
// admin/settings.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/controllers/admin/user.js

@ -1,5 +1,5 @@
// admin/user.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/controllers/auth.js

@ -1,5 +1,5 @@
// auth.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

158
app/controllers/comment.js

@ -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;
},
};

78
app/controllers/email.js

@ -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;
},
};

64
app/controllers/hive.js

@ -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;
},
};

72
app/controllers/hive/kaleidoscope.js

@ -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;
};

17
app/controllers/home.js

@ -1,5 +1,5 @@
// home.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -65,17 +65,8 @@ class HomeController extends SiteController {
res.render('policy/view');
}
async getHome (req, res, next) {
const { post: postService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.featuredPosts = await postService.getFeaturedPosts(3);
res.locals.posts = await postService.getPosts(res.locals.pagination);
res.render('index');
} catch (error) {
return next(error);
}
async getHome (req, res) {
res.render('index');
}
}
@ -87,4 +78,4 @@ module.exports = {
let controller = new HomeController(dtp);
return controller;
},
};
};

2
app/controllers/image.js

@ -1,5 +1,5 @@
// page.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

4
app/controllers/manifest.js

@ -1,5 +1,5 @@
// manifest.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -52,7 +52,7 @@ class ManifestController extends SiteController {
[512, 384, 256, 192, 144, 96, 72, 48, 32, 16].forEach((size) => {
manifest.icons.push({
src: `/img/icon/icon-${size}x${size}.png`,
src: `/img/icon/${this.dtp.config.site.domainKey}/icon-${size}x${size}.png`,
sizes: `${size}x${size}`,
type: 'image/png'
});

104
app/controllers/newsletter.js

@ -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;
},
};

70
app/controllers/user.js

@ -1,5 +1,5 @@
// user.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -31,6 +31,12 @@ class UserController extends SiteController {
dtp.app.use('/user', router);
const authRequired = sessionService.authCheckMiddleware({ requireLogin: true });
const otpSetup = otpAuthService.middleware('Account', {
adminRequired: false,
otpRequired: true,
otpRedirectURL: async (req) => { return `/user/${req.user._id}`; },
});
const otpMiddleware = otpAuthService.middleware('Account', {
adminRequired: false,
otpRequired: false,
@ -58,31 +64,52 @@ class UserController extends SiteController {
router.param('userId', this.populateUser.bind(this));
router.post('/:userId/profile-photo',
router.post(
'/:userId/profile-photo',
limiterService.create(limiterService.config.user.postProfilePhoto),
checkProfileOwner,
upload.single('imageFile'),
this.postProfilePhoto.bind(this),
);
router.post('/:userId/settings',
router.post(
'/:userId/settings',
limiterService.create(limiterService.config.user.postUpdateSettings),
checkProfileOwner,
upload.none(),
this.postUpdateSettings.bind(this),
);
router.post('/',
router.post(
'/',
limiterService.create(limiterService.config.user.postCreate),
this.postCreateUser.bind(this),
);
router.get('/:userId/settings',
router.get(
'/:userId/otp-setup',
limiterService.create(limiterService.config.user.getOtpSetup),
otpSetup,
this.getOtpSetup.bind(this),
);
router.get(
'/:userId/otp-disable',
limiterService.create(limiterService.config.user.getOtpDisable),
authRequired,
this.getOtpDisable.bind(this),
);
router.get(
'/:userId/settings',
limiterService.create(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
checkProfileOwner,
this.getUserSettingsView.bind(this),
);
router.get('/:userId',
router.get(
'/:userId',
limiterService.create(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
@ -90,7 +117,8 @@ class UserController extends SiteController {
this.getUserView.bind(this),
);
router.delete('/:userId/profile-photo',
router.delete(
'/:userId/profile-photo',
limiterService.create(limiterService.config.user.deleteProfilePhoto),
authRequired,
checkProfileOwner,
@ -198,12 +226,7 @@ class UserController extends SiteController {
await userService.updateSettings(req.user, req.body);
displayList.showNotification(
'Member account settings updated successfully.',
'success',
'bottom-center',
6000,
);
displayList.reload();
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to update account settings', { error });
@ -214,8 +237,29 @@ class UserController extends SiteController {
}
}
async getOtpSetup (req, res) {
res.render('user/otp-setup-complete');
}
async getOtpDisable (req, res) {
const { otpAuth: otpAuthService } = this.dtp.services;
try {
await otpAuthService.destroyOtpSession(req, 'Account');
await otpAuthService.removeForUser(req.user, 'Account');
res.render('user/otp-disabled');
} catch (error) {
this.log.error('failed to disable OTP service for Account', { error });
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async getUserSettingsView (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services;
try {
res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account');
res.locals.startTab = req.query.st || 'watch';
res.render('user/settings');
} catch (error) {

2
app/controllers/welcome.js

@ -1,5 +1,5 @@
// welcome.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

25
app/models/category.js

@ -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);

2
app/models/chat-message.js

@ -1,5 +1,5 @@
// chat-message.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License Apache-2.0
'use strict';

2
app/models/comment.js

@ -1,5 +1,5 @@
// comment.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/connect-token.js

@ -1,5 +1,5 @@
// connect-token.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/content-report.js

@ -1,5 +1,5 @@
// content-report.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/content-vote.js

@ -1,5 +1,5 @@
// content-vote.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

37
app/models/core-node-request.js

@ -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);

18
app/models/core-node.js

@ -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);

2
app/models/csrf-token.js

@ -1,5 +1,5 @@
// csrf-token.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/email-blacklist.js

@ -1,5 +1,5 @@
// email-blacklist.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/email-body.js

@ -1,5 +1,5 @@
// email-body.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

19
app/models/email-log.js

@ -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);

18
app/models/email-verify.js

@ -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);

2
app/models/email.js

@ -1,5 +1,5 @@
// email.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/image.js

@ -1,5 +1,5 @@
// image.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/lib/geo-types.js

@ -1,5 +1,5 @@
// geo-types.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/lib/resource-stats.js

@ -1,5 +1,5 @@
// lib/resource-stats.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/log.js

@ -1,5 +1,5 @@
// log.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/net-host-stats.js

@ -1,5 +1,5 @@
// net-host-stats.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/net-host.js

@ -1,5 +1,5 @@
// net-host.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

21
app/models/newsletter-recipient.js

@ -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);

31
app/models/newsletter.js

@ -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);

2
app/models/otp-account.js

@ -1,5 +1,5 @@
// otp-account.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

29
app/models/page.js

@ -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);

36
app/models/post.js

@ -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);

2
app/models/resource-view.js

@ -1,5 +1,5 @@
// resource-view.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/resource-visit.js

@ -1,5 +1,5 @@
// resource-visit.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/user-block.js

@ -1,5 +1,5 @@
// user-block.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/models/user-notification.js

@ -1,5 +1,5 @@
// user-notification.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

16
app/models/user.js

@ -1,5 +1,5 @@
// user.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -10,6 +10,8 @@ const Schema = mongoose.Schema;
const { ResourceStats, ResourceStatsDefaults } = require('./lib/resource-stats');
const DTP_THEME_LIST = ['dtp-light', 'dtp-dark'];
const UserFlagsSchema = new Schema({
isAdmin: { type: Boolean, default: false, required: true },
isModerator: { type: Boolean, default: false, required: true },
@ -20,8 +22,11 @@ const UserPermissionsSchema = new Schema({
canChat: { type: Boolean, default: true, required: true },
canComment: { type: Boolean, default: true, required: true },
canReport: { type: Boolean, default: true, required: true },
canAuthorPages: { type: Boolean, default: false, required: true },
canAuthorPosts: { type: Boolean, default: false, required: true },
});
const UserOptInSchema = new Schema({
system: { type: Boolean, default: true, required: true },
marketing: { type: Boolean, default: true, required: true },
});
const UserSchema = new Schema({
@ -32,14 +37,15 @@ const UserSchema = new Schema({
passwordSalt: { type: String, required: true },
password: { type: String, required: true },
displayName: { type: String },
bio: { type: String, maxlength: 300 },
picture: {
large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' },
},
flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false },
optIn: { type: UserOptInSchema, required: true, select: false },
theme: { type: String, enum: DTP_THEME_LIST, default: 'dtp-light', required: true },
stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
});
module.exports = mongoose.model('User', UserSchema);
module.exports = mongoose.model('User', UserSchema);

2
app/services/cache.js

@ -1,5 +1,5 @@
// cache.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/chat.js

@ -1,5 +1,5 @@
// chat.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/comment.js

@ -1,5 +1,5 @@
// comment.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/content-report.js

@ -1,5 +1,5 @@
// content-report.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/content-vote.js

@ -1,5 +1,5 @@
// content-vote.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

97
app/services/core-node.js

@ -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); },
};

2
app/services/crypto.js

@ -1,5 +1,5 @@
// crypto.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/csrf-token.js

@ -1,5 +1,5 @@
// csrf-token.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

9
app/services/display-engine.js

@ -1,5 +1,5 @@
// display-engine.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -109,6 +109,13 @@ class DisplayList {
params: { href },
});
}
reload ( ) {
this.commands.push({
action: 'reload',
params: { },
});
}
}
class DisplayEngineService extends SiteService {

86
app/services/email.js

@ -1,22 +1,23 @@
// email.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
const pug = require('pug');
const nodemailer = require('nodemailer');
const uuidv4 = require('uuid').v4;
const mongoose = require('mongoose');
const EmailBlacklist = mongoose.model('EmailBlacklist');
const EmailVerify = mongoose.model('EmailVerify');
const EmailLog = mongoose.model('EmailLog');
const disposableEmailDomains = require('disposable-email-provider-domains');
const emailValidator = require('email-validator');
const emailDomainCheck = require('email-domain-check');
const { SiteService } = require('../../lib/site-lib');
const { SiteService, SiteError } = require('../../lib/site-lib');
class EmailService extends SiteService {
@ -27,7 +28,8 @@ class EmailService extends SiteService {
async start ( ) {
await super.start();
if (process.env.DTP_EMAIL_ENABLED !== 'enabled') {
if (process.env.DTP_EMAIL_SERVICE !== 'enabled') {
this.log.info("DTP_EMAIL_SERVICE is disabled, the system can't send email and will not try.");
return;
}
@ -51,17 +53,35 @@ class EmailService extends SiteService {
this.templates = {
html: {
welcome: pug.compileFile(path.join(this.dtp.config.root, 'app', 'templates', 'html', 'welcome.pug')),
userEmail: this.loadAppTemplate('html', 'user-email.pug'),
welcome: this.loadAppTemplate('html', 'welcome.pug'),
},
text: {
welcome: pug.compileFile(path.join(this.dtp.config.root, 'app', 'templates', 'text', 'welcome.pug')),
userEmail: this.loadAppTemplate('text', 'user-email.pug'),
welcome: this.loadAppTemplate('text', 'welcome.pug'),
},
};
}
async renderTemplate (templateId, templateType, templateModel) {
this.log.debug('rendering email template', { templateId, templateType });
return this.templates[templateType][templateId](templateModel);
}
async send (message) {
const NOW = new Date();
this.log.info('sending email', { to: message.to, subject: message.subject });
await this.transport.sendMail(message);
const response = await this.transport.sendMail(message);
await EmailLog.create({
created: NOW,
from: message.from,
to: message.to,
to_lc: message.to.toLowerCase(),
subject: message.subject,
messageId: response.messageId,
});
}
async checkEmailAddress (emailAddress) {
@ -97,8 +117,52 @@ class EmailService extends SiteService {
return false;
}
async renderTemplate (templateId, templateType, message) {
return this.templates[templateType][templateId](message);
async createVerificationToken (user) {
const NOW = new Date();
const verify = new EmailVerify();
verify.created = NOW;
verify.user = user._id;
verify.token = uuidv4();
await verify.save();
this.log.info('created email verification token for user', { user: user._id });
return verify.toObject();
}
async verifyToken (token) {
const NOW = new Date();
const { user: userService } = this.dtp.services;
// fetch the token from the db
const emailVerify = await EmailVerify
.findOne({ token: token })
.populate(this.populateEmailVerify)
.lean();
// verify that the token is at least valid (it exists)
if (!emailVerify) {
this.log.error('email verify token not found', { token });
throw new SiteError(403, 'Email verification token is invalid');
}
// verify that it hasn't already been verified (user clicked link more than once)
if (emailVerify.verified) {
this.log.error('email verify token already claimed', { token });
throw new SiteError(403, 'Email verification token is invalid');
}
this.log.info('marking user email verified', { userId: emailVerify.user._id });
await userService.setEmailVerification(emailVerify.user, true);
await EmailVerify.updateOne({ _id: emailVerify._id }, { $set: { verified: NOW } });
}
async removeVerificationTokensForUser (user) {
this.log.info('removing all pending email address verification tokens for user', { user: user._id });
await EmailVerify.deleteMany({ user: user._id });
}
}

2
app/services/host-cache.js

@ -1,5 +1,5 @@
// host-cache.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/image.js

@ -1,5 +1,5 @@
// minio.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

3
app/services/job-queue.js

@ -1,5 +1,5 @@
// job-queue.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -46,7 +46,6 @@ class JobQueueService extends SiteService {
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD,
keyPrefix: process.env.REDIS_KEY_PREFIX,
lazyConnect: true,
},
defaultJobOptions,
});

2
app/services/limiter.js

@ -1,5 +1,5 @@
// limiter.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/log.js

@ -1,5 +1,5 @@
// log.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/markdown.js

@ -1,5 +1,5 @@
// markdown.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/media.js

@ -1,5 +1,5 @@
// article.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/minio.js

@ -1,5 +1,5 @@
// minio.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

123
app/services/newsletter.js

@ -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); },
};

191
app/services/oauth2.js

@ -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); },
};

10
app/services/otp-auth.js

@ -1,5 +1,5 @@
// otp-auth.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
@ -213,6 +213,14 @@ class OtpAuthService extends SiteService {
return true;
}
async isUserProtected (user, serviceName) {
const account = await OtpAccount.findOne({ user: user._id, service: serviceName });
if (!account) {
return false;
}
return true;
}
async removeForUser (user) {
return await OtpAccount.deleteMany({ user: user });
}

173
app/services/page.js

@ -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); },
};

61
app/services/phone.js

@ -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); },
};

2
app/services/resource.js

@ -1,5 +1,5 @@
// resource.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/session.js

@ -1,5 +1,5 @@
// session.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/sms.js

@ -1,5 +1,5 @@
// sms.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

2
app/services/user-notification.js

@ -1,5 +1,5 @@
// user-notification.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';

132
app/services/user.js

@ -1,9 +1,11 @@
// user.js
// Copyright (C) 2021 Digital Telepresence, LLC
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
const mongoose = require('mongoose');
const User = mongoose.model('User');
@ -24,6 +26,8 @@ class UserService {
this.dtp = dtp;
this.log = new SiteLog(dtp, `svc:${module.exports.slug}`);
this.reservedNames = require(path.join(this.dtp.config.root, 'config', 'reserved-names'));
this.populateUser = [
{
path: 'picture.large',
@ -101,13 +105,18 @@ class UserService {
canChat: true,
canComment: true,
canReport: true,
canAuthorPages: false,
canAuthorPosts: false,
};
user.optIn = {
system: true,
marketing: false,
};
this.log.info('creating new user account', { email: userDefinition.email });
await user.save();
await this.sendWelcomeEmail(user);
return user.toObject();
} catch (error) {
this.log.error('failed to create user', { error });
@ -115,6 +124,71 @@ class UserService {
}
}
async sendWelcomeEmail (user) {
const { email: emailService } = this.dtp.services;
/*
* Remove all pending EmailVerify tokens for the User.
*/
await emailService.removeVerificationTokensForUser(user);
/*
* Create the new/only EmailVerify token for the user. This will be the only
* token accepted. Previous emails sent (if they were received) are invalid
* after this.
*/
const verifyToken = await emailService.createVerificationToken(user);
/*
* Send the welcome email using the new EmailVerify token so it can
* construct a new, valid link to use for verifying the email address.
*/
const templateModel = {
site: this.dtp.config.site,
recipient: user,
emailVerifyToken: verifyToken.token,
};
const message = {
from: process.env.DTP_EMAIL_SMTP_FROM,
to: user.email,
subject: `Welcome to ${this.dtp.config.site.name}!`,
html: await emailService.renderTemplate('welcome', 'html', templateModel),
text: await emailService.renderTemplate('welcome', 'text', templateModel),
};
await emailService.send(message);
}
async setEmailVerification (user, isVerified) {
await User.updateOne(
{ _id: user._id },
{
$set: { 'flags.isEmailVerified': isVerified },
},
);
}
async emailOptOut (userId, category) {
userId = mongoose.Types.ObjectId(userId);
const user = await this.getUserAccount(userId);
if (!user) {
throw new SiteError(406, 'Invalid opt-out token');
}
const updateOp = { $set: { } };
switch (category) {
case 'marketing':
updateOp.$set['optIn.marketing'] = false;
break;
case 'system':
updateOp.$set['optIn.system'] = false;
break;
default:
throw new SiteError(406, 'Invalid opt-out category');
}
await User.updateOne({ _id: userId }, updateOp);
}
async update (user, userDefinition) {
if (!user.flags.canLogin) {
throw SiteError(403, 'Invalid user account operation');
@ -125,9 +199,6 @@ class UserService {
const username_lc = userDefinition.username.toLowerCase();
userDefinition.displayName = striptags(userDefinition.displayName.trim());
if (userDefinition.bio) {
userDefinition.bio = striptags(userDefinition.bio.trim());
}
this.log.info('updating user', { userDefinition });
await User.updateOne(
@ -137,7 +208,8 @@ class UserService {
username: userDefinition.username,
username_lc,
displayName: userDefinition.displayName,
bio: userDefinition.bio,
'optIn.system': userDefinition['optIn.system'] === 'on',
'optIn.marketing': userDefinition['optIn.marketing'] === 'on',
},
},
);
@ -149,9 +221,6 @@ class UserService {
const username_lc = userDefinition.username.toLowerCase();
userDefinition.displayName = striptags(userDefinition.displayName.trim());
if (userDefinition.bio) {
userDefinition.bio = striptags(userDefinition.bio.trim());
}
this.log.info('updating user for admin', { userDefinition });
await User.updateOne(
@ -161,15 +230,12 @@ class UserService {
username: userDefinition.username,
username_lc,
displayName: userDefinition.displayName,
bio: userDefinition.bio,
'flags.isAdmin': userDefinition.isAdmin === 'on',
'flags.isModerator': userDefinition.isModerator === 'on',
'permissions.canLogin': userDefinition.canLogin === 'on',
'permissions.canChat': userDefinition.canChat === 'on',
'permissions.canComment': userDefinition.canComment === 'on',
'permissions.canReport': userDefinition.canReport === 'on',
'permissions.canAuthorPages': userDefinition.canAuthorPages === 'on',
'permissions.canAuthorPosts': userDefinition.canAuthorPosts === 'on',
},
},
);
@ -181,7 +247,6 @@ class UserService {
const username_lc = userDefinition.username.toLowerCase();
userDefinition.displayName = striptags(userDefinition.displayName.trim());
userDefinition.bio = striptags(userDefinition.bio.trim());
this.log.info('updating user settings', { userDefinition });
await User.updateOne(
@ -191,7 +256,7 @@ class UserService {
username: userDefinition.username,
username_lc,
displayName: userDefinition.displayName,
bio: userDefinition.bio,
theme: userDefinition.theme || 'dtp-light',
},
},
);
@ -215,10 +280,10 @@ class UserService {
{ username_lc: accountUsername },
]
})
.select('+passwordSalt +password +flags')
.select('+passwordSalt +password +flags +optIn +permissions')
.lean();
if (!user) {
throw new SiteError(404, 'Member account not found');
throw new SiteError(404, 'Member credentials are invalid');
}
const maskedPassword = crypto.maskPassword(
@ -226,7 +291,7 @@ class UserService {
account.password,
);
if (maskedPassword !== user.password) {
throw new SiteError(403, 'Account credentials do not match');
throw new SiteError(403, 'Member credentials are invalid');
}
// remove these critical fields from the user object
@ -328,7 +393,7 @@ class UserService {
async getUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +picture')
.select('+email +flags +permissions +optIn +picture')
.populate(this.populateUser)
.lean();
if (!user) {
@ -345,7 +410,7 @@ class UserService {
const users = await User
.find(search)
.sort({ username_lc: 1 })
.select('+email +flags +permissions')
.select('+email +flags +permissions +optIn')
.skip(pagination.skip)
.limit(pagination.cpp)
.lean()
@ -375,7 +440,7 @@ class UserService {
username = username.trim().toLowerCase();
const user = await User
.findOne({ username_lc: username })
.select('_id created username username_lc displayName bio picture header')
.select('_id created username username_lc displayName picture header')
.populate(this.populateUser)
.lean();
return user;
@ -384,7 +449,7 @@ class UserService {
async getRecent (maxCount = 3) {
const users = User
.find()
.select('_id created username username_lc displayName bio picture')
.select('_id created username username_lc displayName picture')
.sort({ created: -1 })
.limit(maxCount)
.lean();
@ -468,28 +533,7 @@ class UserService {
if (!username || (typeof username !== 'string') || (username.length === 0)) {
throw new SiteError(406, 'Invalid username');
}
const reservedNames = [
'about',
'admin',
'auth',
'digitaltelepresence',
'dist',
'dtp',
'fontawesome',
'fonts',
'img',
'image',
'less',
'manifest.json',
'moment',
'newsletter',
'numeral',
'socket.io',
'uikit',
'user',
'welcome',
];
if (reservedNames.includes(username.trim().toLowerCase())) {
if (this.reservedNames.includes(username.trim().toLowerCase())) {
throw new SiteError(403, 'That username is reserved for system use');
}

16
app/templates/common/html/footer.pug

@ -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

3
app/templates/common/html/header.pug

@ -1 +1,2 @@
.greeting Dear #{voter.name},
.common-title= emailTitle || `Greetings from ${site.name}!`
.common-slogan= site.description

15
app/templates/common/text/footer.pug

@ -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

3
app/templates/common/text/header.pug

@ -1 +1,2 @@
| Dear #{voter.name},
| Dear #{recipient.displayName || recipient.username},
|

3
app/templates/html/user-email.pug

@ -0,0 +1,3 @@
extends ../layouts/html/system-message
block message-body
.content-message!= htmlMessage

31
app/templates/html/welcome.pug

@ -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.

106
app/templates/layouts/html/system-message.pug

@ -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

4
app/templates/layouts/library.pug

@ -0,0 +1,4 @@
-
function formatCount (count) {
return numeral(count).format((count > 1000) ? '0,0.0a' : '0,0');
}

10
app/templates/layouts/text/system-message.pug

@ -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

5
app/templates/text/user-email.pug

@ -0,0 +1,5 @@
extends ../layouts/text/system-message
block content
|
| #{textMessage}
|

14
app/templates/text/welcome.pug

@ -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.
|

17
app/views/admin/category/editor.pug

@ -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'

21
app/views/admin/category/index.pug

@ -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.

23
app/views/admin/components/menu.pug

@ -14,24 +14,6 @@ ul.uk-nav.uk-nav-default
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'post') })
a(href="/admin/post")
span.nav-item-icon
i.fas.fa-pen
span.uk-margin-small-left Posts
li(class={ 'uk-active': (adminView === 'page') })
a(href="/admin/page")
span.nav-item-icon
i.fas.fa-file
span.uk-margin-small-left Pages
li(class={ 'uk-active': (adminView === 'newsletter') })
a(href="/admin/newsletter")
span.nav-item-icon
i.fas.fa-newspaper
span.uk-margin-small-left Newsletter
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'user') })
a(href="/admin/user")
span.nav-item-icon
@ -45,6 +27,11 @@ ul.uk-nav.uk-nav-default
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'core-node') })
a(href="/admin/core-node")
span.nav-item-icon
i.fas.fa-project-diagram
span.uk-margin-small-left Core Nodes
li(class={ 'uk-active': (adminView === 'host') })
a(href="/admin/host")
span.nav-item-icon

18
app/views/admin/core-node/connect.pug

@ -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

14
app/views/admin/core-node/index.pug

@ -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…
Cancel
Save