Browse Source

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

develop
Rob Colbert 10 months ago
parent
commit
108428a198
  1. 56
      .env.default
  2. 2
      README.md
  3. 2
      app/controllers/admin/announcement.js
  4. 5
      app/controllers/admin/content-report.js
  5. 2
      app/controllers/admin/core-node.js
  6. 2
      app/controllers/admin/core-user.js
  7. 2
      app/controllers/admin/job-queue.js
  8. 19
      app/controllers/admin/service-node.js
  9. 23
      app/controllers/announcement.js
  10. 18
      app/controllers/auth.js
  11. 413
      app/controllers/chat.js
  12. 157
      app/controllers/comment.js
  13. 4
      app/controllers/email.js
  14. 70
      app/controllers/form.js
  15. 2
      app/controllers/home.js
  16. 8
      app/controllers/image.js
  17. 2
      app/controllers/manifest.js
  18. 7
      app/controllers/newsletter.js
  19. 78
      app/controllers/notification.js
  20. 31
      app/controllers/user.js
  21. 2
      app/controllers/welcome.js
  22. 11
      app/models/announcement.js
  23. 64
      app/models/attachment.js
  24. 19
      app/models/chat-message.js
  25. 30
      app/models/chat-room-invite.js
  26. 44
      app/models/chat-room.js
  27. 12
      app/models/comment.js
  28. 2
      app/models/core-user.js
  29. 39
      app/models/emoji-reaction.js
  30. 13
      app/models/lib/resource-stats.js
  31. 6
      app/models/resource-view.js
  32. 46
      app/models/sticker.js
  33. 12
      app/models/user-notification.js
  34. 2
      app/models/user.js
  35. 5
      app/services/announcement.js
  36. 186
      app/services/attachment.js
  37. 733
      app/services/chat.js
  38. 53
      app/services/comment.js
  39. 2
      app/services/content-report.js
  40. 21
      app/services/content-vote.js
  41. 270
      app/services/core-node.js
  42. 7
      app/services/display-engine.js
  43. 1
      app/services/host-cache.js
  44. 2
      app/services/image.js
  45. 18
      app/services/limiter.js
  46. 1
      app/services/markdown.js
  47. 1
      app/services/minio.js
  48. 11
      app/services/oauth2.js
  49. 3
      app/services/otp-auth.js
  50. 2
      app/services/session.js
  51. 2
      app/services/sms.js
  52. 204
      app/services/sticker.js
  53. 94
      app/services/user-notification.js
  54. 39
      app/services/user.js
  55. 19
      app/views/admin/service-node/editor.pug
  56. 14
      app/views/announcement/components/announcement.pug
  57. 11
      app/views/announcement/view.pug
  58. 151
      app/views/chat/components/input-form.pug
  59. 4
      app/views/chat/components/message-standalone.pug
  60. 41
      app/views/chat/components/message.pug
  61. 8
      app/views/chat/components/reaction-button.pug
  62. 17
      app/views/chat/components/room-list.pug
  63. 12
      app/views/chat/components/user-list-entry.pug
  64. 22
      app/views/chat/index.pug
  65. 39
      app/views/chat/layouts/room.pug
  66. 66
      app/views/chat/room/editor.pug
  67. 22
      app/views/chat/room/form/invite-member.pug
  68. 29
      app/views/chat/room/index.pug
  69. 25
      app/views/chat/room/invite/components/invite-list-item.pug
  70. 6
      app/views/chat/room/invite/components/invite-list.pug
  71. 34
      app/views/chat/room/invite/index.pug
  72. 59
      app/views/chat/room/invite/view.pug
  73. 93
      app/views/chat/room/view.pug
  74. 2
      app/views/comment/components/comment-list-standalone.pug
  75. 9
      app/views/comment/components/comment-list.pug
  76. 2
      app/views/comment/components/comment-standalone.pug
  77. 31
      app/views/comment/components/comment.pug
  78. 29
      app/views/comment/components/composer.pug
  79. 2
      app/views/comment/components/reply-list-standalone.pug
  80. 38
      app/views/comment/components/section.pug
  81. 3
      app/views/components/button-icon.pug
  82. 7
      app/views/components/library.pug
  83. 9
      app/views/components/navbar.pug
  84. 12
      app/views/components/off-canvas.pug
  85. 43
      app/views/kaleidoscope/components/event.pug
  86. 14
      app/views/layouts/main.pug
  87. 14
      app/views/notification/index.pug
  88. 2
      app/views/sticker/components/sticker-standalone.pug
  89. 19
      app/views/sticker/components/sticker.pug
  90. 69
      app/views/sticker/index.pug
  91. 2
      app/views/sticker/menu.pug
  92. 26
      app/views/sticker/view.pug
  93. 8
      app/views/user/components/attribution-header.pug
  94. 30
      app/views/user/components/profile-icon.pug
  95. 66
      app/workers/chat.js
  96. 51
      app/workers/chat/job/chat-room-clear.js
  97. 102
      app/workers/chat/job/chat-room-delete.js
  98. 2
      app/workers/host-services.js
  99. 85
      app/workers/media.js
  100. 72
      app/workers/media/job/attachment-delete.js

56
.env.default

@ -9,6 +9,45 @@ DTP_SITE_DOMAIN_KEY=
DTP_SITE_COMPANY=Digital Telepresence, LLC
DTP_PASSWORD_SALT=
DTP_CORE_AUTH_SCHEME=http
DTP_CORE_AUTH_HOST=localhost:3000
DTP_CORE_AUTH_PASSWORD_LEN=64
DTP_IMAGE_WORK_PATH=/tmp/yourapp/image-work
DTP_VIDEO_WORK_PATH=/tmp/yourapp/video-work
DTP_STICKER_WORK_PATH=/tmp/yourapp/sticker-work
DTP_ATTACHMENT_WORK_PATH=/tmp/yourapp/attachment-work
#
# Set this to "enabled" to use NVIDIA GPU acceleration. Setting this to enabled
# without a properly-configured NVIDIA GPU will cause processing jobs to fail.
#
DTP_GPU_ACCELERATION=disabled
#
# Host Cache configuration
#
DTP_HOST_CACHE_PORT=8010
DTP_HOST_CACHE_PATH=/tmp/dtp-webapp/host-cache
DTP_HOST_CACHE_AUTH_KEY=daf3577a-2ab7-49d5-9b5a-d7e331241cde
DTP_HOST_CACHE_CLEAN_CRON=*/30 * * * * *
#
# Nodemailer SMTP Transport configuration
#
DTP_EMAIL_SERVICE=disabled
DTP_EMAIL_SMTP_HOST=
DTP_EMAIL_SMTP_PORT=465
DTP_EMAIL_SMTP_SECURE=disabled
DTP_EMAIL_SMTP_FROM=
DTP_EMAIL_SMTP_USER=
DTP_EMAIL_SMTP_PASS=
DTP_EMAIL_SMTP_POOL_ENABLED=enabled
DTP_EMAIL_SMTP_POOL_MAX_CONN=5
DTP_EMAIL_SMTP_POOL_MAX_MSGS=100
#
# Mailgun Configuration
#
@ -31,6 +70,7 @@ MONGODB_DATABASE=dtp-webapp
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_PREFIX=
#
# MinIO configuration
@ -39,10 +79,12 @@ REDIS_PASSWORD=
MINIO_ENDPOINT=localhost
MINIO_PORT=9000
MINIO_USE_SSL=disabled
MINIO_ACCESS_KEY=dtp-webapp
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_IMAGE_BUCKET=webapp-images
MINIO_VIDEO_BUCKET=webapp-videos
MINIO_IMAGE_BUCKET=yourapp-images
MINIO_VIDEO_BUCKET=yourapp-videos
MINIO_ATTACHMENT_BUCKET=yourapp-attachments
#
# ExpressJS/HTTP configuration
@ -60,12 +102,12 @@ DTP_LOG_CONSOLE=enabled
DTP_LOG_MONGODB=enabled
DTP_LOG_FILE=enabled
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_FILE_PATH=/tmp/dtp-yourapp/logs
DTP_LOG_FILE_NAME_APP=yourapp-app.log
DTP_LOG_FILE_NAME_HTTP=yourapp-access.log
DTP_LOG_DEBUG=enabled
DTP_LOG_INFO=enabled
DTP_LOG_WARN=enabled
DTP_LOG_HTTP_FORMAT=combined
DTP_LOG_HTTP_FORMAT=combined

2
README.md

@ -117,4 +117,4 @@ Redis simply has many different documents to describe it's many different featur
## Software License
The DTP Social engine is licensed under the [Apache 2.0](https://spdx.org/licenses/Apache-2.0.html) open source software license. See [LICENSE](LICENSE) for more information.
DTP Social and the DTP Phoenix Engine and framework are licensed under the [Apache 2.0](https://spdx.org/licenses/Apache-2.0.html) open source software license. See [LICENSE](LICENSE) for more information.

2
app/controllers/admin/announcement.js

@ -87,7 +87,7 @@ class AnnouncementAdminController extends SiteController {
try {
const displayList = this.createDisplayList('delete-announcement');
await announcementService.remove(res.locals.announcement);
displayList.reloadView();
displayList.reload();
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to delete announcement', { error });

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

@ -5,9 +5,8 @@
'use strict';
const express = require('express');
const multer = require('multer');
const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib');
const { SiteController } = require('../../../lib/site-lib');
class ContentReportController extends SiteController {
@ -16,7 +15,7 @@ class ContentReportController extends SiteController {
}
async start ( ) {
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/admin-content-report` });
const upload = this.createMulter();
const router = express.Router();
router.use(async (req, res, next) => {

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

@ -16,8 +16,6 @@ class CoreNodeController extends SiteController {
}
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';

2
app/controllers/admin/core-user.js

@ -16,8 +16,6 @@ class CoreUserController extends SiteController {
}
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';

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

@ -1,6 +1,6 @@
// admin/job-queue.js
// Copyright (C) 2022 DTP Technologies, LLC
// All Rights Reserved
// License: Apache-2.0
'use strict';

19
app/controllers/admin/service-node.js

@ -32,6 +32,8 @@ class ServiceNodeController extends SiteController {
router.get('/:clientId', this.getClientView.bind(this));
router.get('/', this.getIndex.bind(this));
router.delete('/:clientId', this.deleteClient.bind(this));
return router;
}
@ -106,6 +108,23 @@ class ServiceNodeController extends SiteController {
return next(error);
}
}
async deleteClient (req, res) {
const { oauth2: oauth2Service } = this.dtp.services;
try {
await oauth2Service.removeClient(res.locals.serviceNode);
const displayList = this.createDisplayList('delete-newsletter');
displayList.navigateTo('/admin/service-node');
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to delete client', { clientId: res.locals.serviceNode._id, error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {

23
app/controllers/announcement.js

@ -15,11 +15,22 @@ class AnnouncementController extends SiteController {
}
async start ( ) {
const { comment: commentService } = this.dtp.services;
const router = express.Router();
this.dtp.app.use('/announcement', router);
const upload = this.createMulter();
router.use(async (req, res, next) => {
res.locals.currentView = 'announcement';
return next();
});
router.param('announcementId', this.populateAnnouncementId.bind(this));
router.post('/:announcementId/comment', upload.none(), commentService.commentCreateHandler('Announcement', 'announcement'));
router.get('/:announcementId', this.getAnnouncementView.bind(this));
router.get('/', this.getHome.bind(this));
@ -40,8 +51,16 @@ class AnnouncementController extends SiteController {
}
}
async getAnnouncementView (req, res) {
res.render('announcement/view');
async getAnnouncementView (req, res, next) {
const { comment: commentService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.comments = await commentService.getForResource(res.locals.announcement, ['published'], res.locals.pagination);
res.render('announcement/view');
} catch (error) {
this.log.error('failed to render announcement view', { error });
return next(error);
}
}
async getHome (req, res, next) {

18
app/controllers/auth.js

@ -6,7 +6,6 @@
const express = require('express');
const mongoose = require('mongoose');
const multer = require('multer');
const passport = require('passport');
const uuidv4 = require('uuid').v4;
@ -25,7 +24,8 @@ class AuthController extends SiteController {
coreNode: coreNodeService,
limiter: limiterService,
} = this.dtp.services;
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${this.component.slug}` });
const upload = this.createMulter();
const router = express.Router();
this.dtp.app.use('/auth', router);
@ -35,18 +35,18 @@ class AuthController extends SiteController {
router.post(
'/otp/enable',
limiterService.create(limiterService.config.auth.postOtpEnable),
limiterService.createMiddleware(limiterService.config.auth.postOtpEnable),
this.postOtpEnable.bind(this),
);
router.post(
'/otp/auth',
limiterService.create(limiterService.config.auth.postOtpAuthenticate),
limiterService.createMiddleware(limiterService.config.auth.postOtpAuthenticate),
this.postOtpAuthenticate.bind(this),
);
router.post(
'/login',
limiterService.create(limiterService.config.auth.postLogin),
limiterService.createMiddleware(limiterService.config.auth.postLogin),
upload.none(),
this.postLogin.bind(this),
);
@ -54,14 +54,14 @@ class AuthController extends SiteController {
router.get(
'/api-token/personal',
authRequired,
limiterService.create(limiterService.config.auth.getPersonalApiToken),
limiterService.createMiddleware(limiterService.config.auth.getPersonalApiToken),
this.getPersonalApiToken.bind(this),
);
router.get(
'/socket-token',
authRequiredNoRedirect,
limiterService.create(limiterService.config.auth.getSocketToken),
limiterService.createMiddleware(limiterService.config.auth.getSocketToken),
this.getSocketToken.bind(this),
);
@ -69,14 +69,14 @@ class AuthController extends SiteController {
router.get(
'/core',
limiterService.create(limiterService.config.auth.getCoreHome),
limiterService.createMiddleware(limiterService.config.auth.getCoreHome),
this.getCoreHome.bind(this),
);
router.get(
'/logout',
authRequired,
limiterService.create(limiterService.config.auth.getLogout),
limiterService.createMiddleware(limiterService.config.auth.getLogout),
this.getLogout.bind(this),
);

413
app/controllers/chat.js

@ -0,0 +1,413 @@
// email.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const { SiteController,/*, SiteError*/
SiteError} = require('../../lib/site-lib');
class ChatController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const {
chat: chatService,
limiter: limiterService,
session: sessionService,
} = this.dtp.services;
const upload = this.createMulter();
const router = express.Router();
this.dtp.app.use('/chat', router);
router.use(
sessionService.authCheckMiddleware({ requireLogin: true }),
chatService.middleware({ maxOwnedRooms: 25, maxJoinedRooms: 50 }),
async (req, res, next) => {
res.locals.currentView = 'chat';
return next();
},
);
router.param('roomId', this.populateRoomId.bind(this));
router.param('inviteId', this.populateInviteId.bind(this));
router.post(
'/room/:roomId/invite/:inviteId/action',
limiterService.createMiddleware(limiterService.config.chat.postRoomInviteAction),
upload.none(),
this.postRoomInviteAction.bind(this),
);
router.post(
'/room/:roomId/invite',
limiterService.createMiddleware(limiterService.config.chat.postRoomInvite),
upload.none(),
this.postRoomInvite.bind(this),
);
router.post(
'/room/:roomId',
limiterService.createMiddleware(limiterService.config.chat.postRoomUpdate),
this.postRoomUpdate.bind(this),
);
router.post(
'/room',
limiterService.createMiddleware(limiterService.config.chat.postRoomCreate),
this.postRoomCreate.bind(this),
);
router.get(
'/room/create',
this.getRoomEditor.bind(this),
);
router.get(
'/room/:roomId/form/:formName',
limiterService.createMiddleware(limiterService.config.chat.getRoomForm),
this.getRoomForm.bind(this),
);
router.get(
'/room/:roomId/invite/:inviteId',
limiterService.createMiddleware(limiterService.config.chat.getRoomInviteView),
this.getRoomInviteView.bind(this),
);
router.get(
'/room/:roomId/invite',
limiterService.createMiddleware(limiterService.config.chat.getRoomInviteView),
this.getRoomInviteHome.bind(this),
);
router.get(
'/room/:roomId/settings',
limiterService.createMiddleware(limiterService.config.chat.getRoomSettings),
this.getRoomSettings.bind(this),
);
router.get(
'/room/:roomId',
limiterService.createMiddleware(limiterService.config.chat.getRoomView),
this.getRoomView.bind(this),
);
router.get(
'/room',
limiterService.createMiddleware(limiterService.config.chat.getRoomHome),
this.getRoomHome.bind(this),
);
router.get(
'/',
limiterService.createMiddleware(limiterService.config.chat.getHome),
this.getHome.bind(this),
);
/*
* DELETE operations
*/
router.delete(
'/room/:roomId/invite/:inviteId',
limiterService.createMiddleware(limiterService.config.chat.deleteInvite),
this.deleteInvite.bind(this),
);
router.delete(
'/room/:roomId',
limiterService.createMiddleware(limiterService.config.chat.deleteRoom),
this.deleteInvite.bind(this),
);
return router;
}
async populateRoomId (req, res, next, roomId) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.room = await chatService.getRoomById(roomId);
if (!res.locals.room) {
throw new SiteError(404, 'Room not found');
}
return next();
} catch (error) {
this.log.error('failed to populate roomId', { roomId, error });
return next(error);
}
}
async populateInviteId (req, res, next, inviteId) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.invite = await chatService.getRoomInviteById(inviteId);
if (!res.locals.invite) {
throw new SiteError(404, 'Invite not found');
}
return next();
} catch (error) {
this.log.error('failed to populate inviteId', { inviteId, error });
return next(error);
}
}
async postRoomInviteAction (req, res) {
const { chat: chatService } = this.dtp.services;
try {
const { response } = req.body;
const displayList = this.createDisplayList('room-invite-action');
this.log.debug('room invite action', { message: req.body });
switch (response) {
case 'accept':
await chatService.acceptRoomInvite(res.locals.invite);
displayList.showNotification(
`Chat room invite accepted`,
'success',
'top-center',
5000,
);
break;
case 'reject':
await chatService.acceptRoomInvite(res.locals.invite);
displayList.showNotification(
`Chat room invite rejected`,
'success',
'top-center',
5000,
);
break;
default:
throw new SiteError(400, 'Must specify invite action');
}
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to execute room invite action', {
inviteId: res.locals.invite._id,
response: req.body.response,
error,
});
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postRoomInvite (req, res) {
const { chat: chatService, user: userService } = this.dtp.services;
this.log.debug('room invite received', { invite: req.body });
if (!req.body.username || !req.body.username.length) {
return res.status(400).json({ success: false, message: 'Please provide a username' });
}
try {
req.body.username = req.body.username.trim().toLowerCase();
while (req.body.username[0] === '@') {
req.body.username = req.body.username.slice(1);
}
if (!req.body.username || !req.body.username.length) {
throw new SiteError(400, 'Please provide a username');
}
const member = await userService.getPublicProfile(req.body.username);
if (!member) {
throw new SiteError(404, `There is no account with username @${req.body.username}`);
}
if (member._id.equals(res.locals.room.owner._id)) {
throw new SiteError(400, "You can't invite yourself.");
}
await chatService.sendRoomInvite(res.locals.room, member, req.body);
const displayList = this.createDisplayList('invite create');
displayList.showNotification(
`Chat room invite sent to ${member.displayName || member.username}!`,
'success',
'top-left',
5000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to create room invitation', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postRoomUpdate (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.room = await chatService.updateRoom(res.locals.room, req.body);
res.redirect(`/chat/room/${res.locals.room._id}`);
} catch (error) {
this.log.error('failed to update chat room', {
// roomId: res.locals.room._id,
error,
});
return next(error);
}
}
async postRoomCreate (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.room = await chatService.createRoom(req.user, req.body);
res.redirect(`/chat/room/${res.locals.room._id}`);
} catch (error) {
this.log.error('failed to create chat room', { error });
return next(error);
}
}
async getRoomEditor (req, res) {
res.render('chat/room/editor');
}
async getRoomForm (req, res, next) {
const validFormNames = [
'invite-member',
];
const formName = req.params.formName;
if (validFormNames.indexOf(formName) === -1) {
return next(new SiteError(404, 'Form not found'));
}
try {
res.render(`chat/room/form/${formName}`);
} catch (error) {
this.log.error('failed to render form', { formName, error });
return next(error);
}
}
async getRoomInviteView (req, res) {
res.render('chat/room/invite/view');
}
async getRoomInviteHome (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.invites = {
new: await chatService.getRoomInvites(res.locals.room, 'new'),
accepted: await chatService.getRoomInvites(res.locals.room, 'accepted'),
rejected: await chatService.getRoomInvites(res.locals.room, 'rejected'),
};
res.render('chat/room/invite');
} catch (error) {
this.log.error('failed to render the room invites view', { error });
return next(error);
}
}
async getRoomSettings (req, res) {
res.render('chat/room/editor');
}
async getRoomView (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.pageTitle = res.locals.room.name;
const pagination = { skip: 0, cpp: 20 };
res.locals.chatMessages = await chatService.getChannelHistory(res.locals.room, pagination);
res.render('chat/room/view');
} catch (error) {
this.log.error('failed to render chat room view', { roomId: req.params.roomId, error });
return next(error);
}
}
async getRoomHome (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.publicRooms = await chatService.getPublicRooms(req.user, res.locals.pagination);
res.render('chat/room/index');
} catch (error) {
this.log.error('failed to render room home', { error });
return next(error);
}
}
async getHome (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
res.locals.pageTitle = 'Chat Home';
res.locals.pagination = this.getPaginationParameters(req, 20);
const roomIds = [ ];
res.locals.ownedChatRooms.forEach((room) => roomIds.push(room._id));
res.locals.joinedChatRooms.forEach((room) => roomIds.push(room._id));
res.locals.timeline = await chatService.getMultiRoomTimeline(roomIds, res.locals.pagination);
res.render('chat/index');
} catch (error) {
this.log.error('failed to render chat home', { error });
return next(error);
}
}
async deleteInvite (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
if (res.locals.room.owner._id.equals(req.user._id)) {
throw new SiteError(403, 'This is not your invitiation');
}
await chatService.deleteRoomInvite(res.locals.invite);
const displayList = this.createDisplayList('delete chat invite');
displayList.removeElement(`li[data-invite-id="${res.locals.invite._id}"]`);
displayList.showNotification(
`Invitation to ${res.locals.invite.member.displayName || res.locals.invite.member.username} deleted successfully`,
'success',
'top-left',
5000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to delete chat room invite', { error });
return next(error);
}
}
async deleteRoom (req, res, next) {
const { chat: chatService } = this.dtp.services;
try {
if (res.locals.room.owner._id.equals(req.user._id)) {
throw new SiteError(403, 'This is not your chat room');
}
await chatService.deleteRoom(res.locals.room);
const displayList = this.createDisplayList('delete chat invite');
displayList.navigateTo('/chat');
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to delete chat room invite', { error });
return next(error);
}
}
}
module.exports = {
slug: 'chat',
name: 'chat',
create: async (dtp) => { return new ChatController(dtp); },
};

157
app/controllers/comment.js

@ -0,0 +1,157 @@
// comment.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const numeral = require('numeral');
const { SiteController, SiteError } = require('../../lib/site-lib');
class CommentController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
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 = module.exports.slug;
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.createMiddleware(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, resourceStats } = 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(resourceStats.upvoteCount).format(resourceStats.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(resourceStats.downvoteCount).format(resourceStats.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');
if (Array.isArray(res.locals.comments) && (res.locals.comments.length > 0)) {
displayList.removeElement(`p#empty-comments-label[data-comment-id="${res.locals.comment._id}"]`);
}
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) => { return new CommentController(dtp); },
};

4
app/controllers/email.js

@ -26,13 +26,13 @@ class EmailController extends SiteController {
router.get(
'/verify',
limiterService.create(limiterService.config.email.getEmailVerify),
limiterService.createMiddleware(limiterService.config.email.getEmailVerify),
this.getEmailVerify.bind(this),
);
router.get(
'/opt-out',
limiterService.create(limiterService.config.email.getEmailOptOut),
limiterService.createMiddleware(limiterService.config.email.getEmailOptOut),
this.getEmailOptOut.bind(this),
);

70
app/controllers/form.js

@ -0,0 +1,70 @@
// email.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
const glob = require('glob');
const express = require('express');
const { SiteController,/*, SiteError*/
SiteError} = require('../../lib/site-lib');
class FormController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const {
chat: chatService,
limiter: limiterService,
session: sessionService,
} = this.dtp.services;
try {
this.forms = glob.sync(path.join(this.dtp.config.root, 'app', 'views', 'form', '*pug')) || [ ];
this.forms = this.forms.map((filename) => path.parse(filename));
} catch (error) {
this.log.error('failed to detect requestable forms', { error });
this.forms = [ ];
// fall through
}
const router = express.Router();
this.dtp.app.use('/form', router);
router.use(
sessionService.authCheckMiddleware({ requireLogin: true }),
chatService.middleware({ maxOwnedRooms: 25, maxJoinedRooms: 50 }),
async (req, res, next) => {
res.locals.currentView = module.exports.slug;
return next();
},
);
router.get(
'/:formSlug',
limiterService.createMiddleware(limiterService.config.form.getForm),
this.getForm.bind(this),
);
}
async getForm (req, res, next) {
const { formSlug } = req.params;
const form = this.forms.find((form) => form.name === formSlug);
if (!form) {
return next(new SiteError(400, 'Invalid form'));
}
res.render(`form/${form.name}`);
}
}
module.exports = {
slug: 'form',
name: 'form',
create: async (dtp) => { return new FormController(dtp); },
};

2
app/controllers/home.js

@ -33,7 +33,7 @@ class HomeController extends SiteController {
router.get('/policy/:policyDocument', this.getPolicyDocument.bind(this));
router.get('/',
limiterService.create(limiterService.config.home.getHome),
limiterService.createMiddleware(limiterService.config.home.getHome),
this.getHome.bind(this),
);
}

8
app/controllers/image.js

@ -8,7 +8,6 @@ const fs = require('fs');
const express = require('express');
const mongoose = require('mongoose');
const multer = require('multer');
const { SiteController/*, SiteError*/ } = require('../../lib/site-lib');
@ -26,8 +25,7 @@ class ImageController extends SiteController {
const router = express.Router();
dtp.app.use('/image', router);
const imageUpload = multer({
dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${this.component.slug}`,
const imageUpload = this.createMulter('uploads', {
limits: {
fileSize: 1024 * 1000 * 12,
},
@ -46,13 +44,13 @@ class ImageController extends SiteController {
);
router.post('/',
limiterService.create(limiterService.config.image.postCreateImage),
limiterService.createMiddleware(limiterService.config.image.postCreateImage),
imageUpload.single('file'),
this.postCreateImage.bind(this),
);
router.get('/:imageId',
limiterService.create(limiterService.config.image.getImage),
limiterService.createMiddleware(limiterService.config.image.getImage),
this.getHostCacheImage.bind(this),
// this.getImage.bind(this),
);

2
app/controllers/manifest.js

@ -27,7 +27,7 @@ class ManifestController extends SiteController {
});
router.get('/',
limiterService.create(limiterService.config.manifest.getManifest),
limiterService.createMiddleware(limiterService.config.manifest.getManifest),
this.getManifest.bind(this),
);
}

7
app/controllers/newsletter.js

@ -5,7 +5,6 @@
'use strict';
const express = require('express');
const multer = require('multer');
const { SiteController } = require('../../lib/site-lib');
@ -19,7 +18,7 @@ class NewsletterController extends SiteController {
const { dtp } = this;
const { limiter: limiterService } = dtp.services;
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${module.exports.slug}` });
const upload = this.createMulter();
const router = express.Router();
dtp.app.use('/newsletter', router);
@ -34,12 +33,12 @@ class NewsletterController extends SiteController {
router.post('/', upload.none(), this.postAddRecipient.bind(this));
router.get('/:newsletterId',
limiterService.create(limiterService.config.newsletter.getView),
limiterService.createMiddleware(limiterService.config.newsletter.getView),
this.getView.bind(this),
);
router.get('/',
limiterService.create(limiterService.config.newsletter.getIndex),
limiterService.createMiddleware(limiterService.config.newsletter.getIndex),
this.getIndex.bind(this),
);
}

78
app/controllers/notification.js

@ -0,0 +1,78 @@
// notification.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const { SiteController, SiteError } = require('../../lib/site-lib');
class NotificationController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const { dtp } = this;
const { limiter: limiterService } = dtp.services;
const router = express.Router();
dtp.app.use('/notification', router);
router.use(async (req, res, next) => {
res.locals.currentView = 'notification';
return next();
});
router.param('notificationId', this.populateNotificationId.bind(this));
router.get(
'/:notificationId',
limiterService.createMiddleware(limiterService.config.notification.getNotificationView),
this.getNotificationView.bind(this),
);
router.get('/',
limiterService.createMiddleware(limiterService.config.notification.getNotificationHome),
this.getNotificationHome.bind(this),
);
}
async populateNotificationId (req, res, next, notificationId) {
const { userNotification: userNotificationService } = this.dtp.services;
try {
res.locals.notification = await userNotificationService.getById(notificationId);
if (!res.locals.notification) {
throw new SiteError(404, 'Notification not found');
}
return next();
} catch (error) {
this.log.error('failed to populate notificationId', { notificationId, error });
return next(error);
}
}
async getNotificationView (req, res) {
res.render('notification/view');
}
async getNotificationHome (req, res, next) {
const { userNotification: userNotificationService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.notifications = await userNotificationService.getForUser(req.user, res.locals.pagination);
res.render('notification/index');
} catch (error) {
this.log.error('failed to render notification home view', { error });
return next(error);
}
}
}
module.exports = {
slug: 'notification',
name: 'notification',
create: async (dtp) => { return new NotificationController(dtp); },
};

31
app/controllers/user.js

@ -6,7 +6,6 @@
const express = require('express');
const mongoose = require('mongoose');
const multer = require('multer');
const { SiteController, SiteError } = require('../../lib/site-lib');
@ -24,7 +23,8 @@ class UserController extends SiteController {
session: sessionService,
} = dtp.services;
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${this.component.slug}` });
const upload = this.createMulter();
const router = express.Router();
dtp.app.use('/user', router);
@ -65,7 +65,7 @@ class UserController extends SiteController {
router.post(
'/core/:coreUserId/settings',
limiterService.create(limiterService.config.user.postUpdateCoreSettings),
limiterService.createMiddleware(limiterService.config.user.postUpdateCoreSettings),
checkProfileOwner,
upload.none(),
this.postUpdateCoreSettings.bind(this),
@ -73,7 +73,7 @@ class UserController extends SiteController {
router.post(
'/:userId/profile-photo',
limiterService.create(limiterService.config.user.postProfilePhoto),
limiterService.createMiddleware(limiterService.config.user.postProfilePhoto),
checkProfileOwner,
upload.single('imageFile'),
this.postProfilePhoto.bind(this),
@ -81,7 +81,7 @@ class UserController extends SiteController {
router.post(
'/:userId/settings',
limiterService.create(limiterService.config.user.postUpdateSettings),
limiterService.createMiddleware(limiterService.config.user.postUpdateSettings),
checkProfileOwner,
upload.none(),
this.postUpdateSettings.bind(this),
@ -89,13 +89,13 @@ class UserController extends SiteController {
router.post(
'/',
limiterService.create(limiterService.config.user.postCreate),
limiterService.createMiddleware(limiterService.config.user.postCreate),
this.postCreateUser.bind(this),
);
router.get(
'/core/:coreUserId/settings',
limiterService.create(limiterService.config.user.getSettings),
limiterService.createMiddleware(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
checkProfileOwner,
@ -103,29 +103,28 @@ class UserController extends SiteController {
);
router.get(
'/core/:coreUserId',
limiterService.create(limiterService.config.user.getUserProfile),
limiterService.createMiddleware(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
checkProfileOwner,
this.getUserView.bind(this),
);
router.get(
'/:userId/otp-setup',
limiterService.create(limiterService.config.user.getOtpSetup),
limiterService.createMiddleware(limiterService.config.user.getOtpSetup),
otpSetup,
this.getOtpSetup.bind(this),
);
router.get(
'/:userId/otp-disable',
limiterService.create(limiterService.config.user.getOtpDisable),
limiterService.createMiddleware(limiterService.config.user.getOtpDisable),
authRequired,
this.getOtpDisable.bind(this),
);
router.get(
'/:userId/settings',
limiterService.create(limiterService.config.user.getSettings),
limiterService.createMiddleware(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
checkProfileOwner,
@ -133,16 +132,15 @@ class UserController extends SiteController {
);
router.get(
'/:userId',
limiterService.create(limiterService.config.user.getUserProfile),
limiterService.createMiddleware(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
checkProfileOwner,
this.getUserView.bind(this),
);
router.delete(
'/:userId/profile-photo',
limiterService.create(limiterService.config.user.deleteProfilePhoto),
limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto),
authRequired,
checkProfileOwner,
this.deleteProfilePhoto.bind(this),
@ -157,9 +155,6 @@ class UserController extends SiteController {
return next(new SiteError(406, 'Invalid User'));
}
try {
if (!req.user._id.equals(userId)) {
return next(new Error('Invalid account ID'));
}
res.locals.userProfile = await userService.getUserAccount(userId);
return next();
} catch (error) {

2
app/controllers/welcome.js

@ -19,7 +19,7 @@ class WelcomeController extends SiteController {
async start ( ) {
const { limiter: limiterService } = this.dtp.services;
const welcomeLimiter = limiterService.create(limiterService.config.welcome);
const welcomeLimiter = limiterService.createMiddleware(limiterService.config.welcome);
captcha.loadFont(path.join(this.dtp.config.root, 'client', 'fonts', 'Dirty Sweb.ttf'));

11
app/models/announcement.js

@ -4,12 +4,20 @@
'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 AnnouncementSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1, expires: '21d' },
created: { type: Date, default: Date.now, required: true, index: -1 },
title: {
icon: {
class: { type: String, default: 'fa-bullhorn', required: true },
@ -18,6 +26,7 @@ const AnnouncementSchema = new Schema({
content: { type: String, required: true },
},
content: { type: String, required: true },
resourceStats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
});
module.exports = mongoose.model('Announcement', AnnouncementSchema);

64
app/models/attachment.js

@ -0,0 +1,64 @@
// attachment.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ATTACHMENT_STATUS_LIST = [
'processing', // the attachment is in the processing queue
'live', // the attachment is available for use
'rejected', // the attachment was rejected (by proccessing queue)
'retired', // the attachment has been retired
];
const AttachmentFileSchema = new Schema({
bucket: { type: String, required: true },
key: { type: String, required: true },
mime: { type: String, required: true },
size: { type: Number, required: true },
etag: { type: String, required: true },
}, {
_id: false,
});
/*
* Attachments are simply files. They can really be any kind of file, but will
* mostly be image, video, and audio files.
*
* owner is the User or CoreUser that uploaded the attachment.
*
* item is the item to which the attachment is attached.
*/
const AttachmentSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: 1 },
status: { type: String, enum: ATTACHMENT_STATUS_LIST, default: 'processing', required: true },
ownerType: { type: String, required: true },
owner: { type: Schema.ObjectId, required: true, index: 1, refPath: 'ownerType' },
itemType: { type: String, required: true },
item: { type: Schema.ObjectId, required: true, index: 1, refPath: 'itemType' },
original: { type: AttachmentFileSchema, required: true, select: false },
encoded: { type: AttachmentFileSchema, required: true },
flags: {
isSensitive: { type: Boolean, default: false, required: true },
},
});
AttachmentSchema.index({
ownerType: 1,
owner: 1,
}, {
name: 'attachment_owner_idx',
});
AttachmentSchema.index({
itemType: 1,
item: 1,
}, {