Browse Source

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

develop
Rob Colbert 11 months ago
parent
commit
d3780f6a19
  1. 4
      .env.default
  2. 1
      .gitignore
  3. 9
      README.md
  4. 3
      app/controllers/admin.js
  5. 55
      app/controllers/admin/otp.js
  6. 48
      app/controllers/admin/settings.js
  7. 19
      app/controllers/admin/user.js
  8. 2
      app/controllers/hive/user.js
  9. 3
      app/controllers/home.js
  10. 108
      app/controllers/user.js
  11. 4
      app/models/announcement.js
  12. 4
      app/models/attachment.js
  13. 4
      app/models/chat-message.js
  14. 4
      app/models/chat-room-invite.js
  15. 4
      app/models/chat-room.js
  16. 4
      app/models/comment.js
  17. 4
      app/models/connect-token.js
  18. 4
      app/models/content-report.js
  19. 4
      app/models/content-vote.js
  20. 4
      app/models/core-node-connect.js
  21. 4
      app/models/core-node-request.js
  22. 4
      app/models/core-node.js
  23. 4
      app/models/core-user.js
  24. 4
      app/models/csrf-token.js
  25. 4
      app/models/email-blacklist.js
  26. 4
      app/models/email-body.js
  27. 4
      app/models/email-log.js
  28. 4
      app/models/email-verify.js
  29. 4
      app/models/email.js
  30. 4
      app/models/emoji-reaction.js
  31. 4
      app/models/feed-entry.js
  32. 4
      app/models/feed.js
  33. 4
      app/models/image.js
  34. 4
      app/models/kaleidoscope-event.js
  35. 4
      app/models/log.js
  36. 4
      app/models/media-router.js
  37. 4
      app/models/media-worker.js
  38. 4
      app/models/net-host-stats.js
  39. 4
      app/models/net-host.js
  40. 4
      app/models/newsletter-recipient.js
  41. 4
      app/models/newsletter.js
  42. 4
      app/models/oauth2-authorization-code.js
  43. 4
      app/models/oauth2-client.js
  44. 4
      app/models/oauth2-token.js
  45. 4
      app/models/otp-account.js
  46. 4
      app/models/resource-view.js
  47. 4
      app/models/resource-visit.js
  48. 4
      app/models/sticker.js
  49. 4
      app/models/user-block.js
  50. 4
      app/models/user-notification.js
  51. 4
      app/models/user-subscription.js
  52. 11
      app/models/user.js
  53. 15
      app/services/chat.js
  54. 11
      app/services/feed.js
  55. 114
      app/services/image.js
  56. 16
      app/services/otp-auth.js
  57. 3
      app/services/session.js
  58. 189
      app/services/user.js
  59. 51
      app/views/admin/components/file-upload-image.pug
  60. 12
      app/views/admin/components/menu.pug
  61. 2
      app/views/admin/core-user/form.pug
  62. 7
      app/views/admin/layouts/main.pug
  63. 28
      app/views/admin/otp/index.pug
  64. 28
      app/views/admin/settings/images.pug
  65. 4
      app/views/admin/user/form.pug
  66. 4
      app/views/admin/user/index.pug
  67. 8
      app/views/components/library.pug
  68. 4
      app/views/components/navbar.pug
  69. 4
      app/views/components/off-canvas.pug
  70. 2
      app/views/layouts/main-sidebar.pug
  71. 10
      app/views/newsroom/components/feed-entry-list-item.pug
  72. 15
      app/views/otp/new-account.pug
  73. 2
      app/views/otp/welcome.pug
  74. 48
      app/views/welcome/core-home.pug
  75. 37
      client/js/site-admin-app.js
  76. 7
      client/less/site/main.less
  77. 9
      client/less/site/markdown.less
  78. 7
      client/less/style.common.less
  79. 4
      dtp-webapp-cli.js
  80. 10
      dtp-webapp.js
  81. 2
      lib/client/js/dtp-log.js
  82. 2
      lib/client/js/dtp-socket.js
  83. 7
      lib/site-common.js
  84. 10
      lib/site-ioserver.js
  85. 22
      lib/site-platform.js
  86. 2
      package.json

4
.env.default

@ -62,6 +62,10 @@ MAILGUN_DOMAIN=
MONGODB_HOST=localhost:27017
MONGODB_DATABASE=dtp-webapp
# For when NODE_ENV=production
MONGODB_USERNAME=mongo-user
MONGODB_PASSWORD=change-me!
MONGODB_OPTIONS=
#
# Redis configuration

1
.gitignore

@ -6,3 +6,4 @@ ssl/*key
data/minio
node_modules
dist
start-local-*

9
README.md

@ -6,6 +6,15 @@ DTP Core implements user account management, platform search, the platform direc
The only qualified operated system for hosting a DTP Core suite is [Ubuntu 20.04 LTS](https://releases.ubuntu.com/20.04/). It is acceptable to run it in a virtual machine for development and testing, but it should be run as close to bare metal as can be had for production environments.
## Host Preparation
The following commands must be exeucted on any host expected to run DTP Framework applications.
```sh
apt -y update && apt -y upgrade
apt -y install linux-headers-generic linux-headers-virtual linux-image-virtual linux-virtual
apt -y install build-essential ffmpeg supervisor
```
## Install Data Tier Components
You will need MongoDB and MinIO installed and running before you can start DTP Core web services.

3
app/controllers/admin.js

@ -51,6 +51,7 @@ class AdminController extends SiteController {
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));
router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));
router.use('/newsroom', await this.loadChild(path.join(__dirname, 'admin', 'newsroom')));
router.use('/otp', await this.loadChild(path.join(__dirname, 'admin', 'otp')));
router.use('/settings', await this.loadChild(path.join(__dirname, 'admin', 'settings')));
router.use('/service-node', await this.loadChild(path.join(__dirname, 'admin', 'service-node')));
router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user')));
@ -83,6 +84,8 @@ class AdminController extends SiteController {
constellation: await coreNodeService.getConstellationStats(),
};
res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`;
res.render('admin/index');
}
}

55
app/controllers/admin/otp.js

@ -0,0 +1,55 @@
// admin/otp.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
// const multer = require('multer');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class OtpAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
// const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${module.exports.slug}` });
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'otp';
return next();
});
// router.param('otp', this.populateOtp.bind(this));
router.get('/', this.getIndex.bind(this));
return router;
}
async getIndex (req, res, next) {
try {
const { otpAuth: otpAuthService } = this.dtp.services;
if (!req.user) {
throw new SiteError(402, "Error getting user");
}
res.locals.tokens = await otpAuthService.getBackupTokens(req.user, "Admin");
res.render('admin/otp/index');
} catch (error) {
this.log.error('failed to get tokens', { error });
return next(error);
}
}
}
module.exports = {
name: 'adminOtp',
slug: 'admin-opt',
create: async (dtp) => { return new OtpAdminController(dtp); },
};

48
app/controllers/admin/settings.js

@ -16,6 +16,13 @@ class SettingsController extends SiteController {
async start ( ) {
const router = express.Router();
const imageUpload = this.createMulter('uploads', {
limits: {
fileSize: 1024 * 1000 * 12,
},
});
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'settings';
@ -23,9 +30,13 @@ class SettingsController extends SiteController {
});
router.post('/', this.postUpdateSettings.bind(this));
router.post('/images/updateSiteIcon', imageUpload.single('imageFile'), this.postUpdateSiteIcon.bind(this));
router.get('/', this.getSettingsView.bind(this));
router.get('/images', this.getImageSettings.bind(this));
return router;
}
@ -47,6 +58,43 @@ class SettingsController extends SiteController {
return next(error);
}
}
async getImageSettings (req, res, next) {
const { image: imageService } = this.dtp.services;
res.locals.adminView = 'image-settings';
res.locals.pageTitle = `Image settings for ${this.dtp.config.site.name}`;
try {
res.locals.siteIcon = await imageService.getSiteIconInfo();
res.render('admin/settings/images');
} catch (error) {
return next(error);
}
}
async postUpdateSiteIcon (req, res) {
const { image: imageService } = this.dtp.services;
try {
const displayList = this.createDisplayList('site-icon');
await imageService.updateSiteIcon(req.body, req.file);
displayList.showNotification(
'Site Icon updated successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({
success: true,
displayList,
});
} catch (error) {
this.log.error('failed to update site icon', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {

19
app/controllers/admin/user.js

@ -22,37 +22,36 @@ class UserController extends SiteController {
return next();
});
router.param('userId', this.populateUserId.bind(this));
router.param('localUserId', this.populateLocalUserId.bind(this));
router.post('/local/:localUserId', this.postUpdateLocalUser.bind(this));
router.get('/local/:localUserId', this.getLocalUserView.bind(this));
router.post('/:userId', this.postUpdateUser.bind(this));
router.get('/:userId', this.getUserView.bind(this));
router.get('/', this.getHomeView.bind(this));
return router;
}
async populateUserId (req, res, next, userId) {
async populateLocalUserId (req, res, next, localUserId) {
const { user: userService } = this.dtp.services;
try {
res.locals.userAccount = await userService.getUserAccount(userId);
res.locals.userAccount = await userService.getLocalUserAccount(localUserId);
return next();
} catch (error) {
return next(error);
}
}
async postUpdateUser (req, res, next) {
async postUpdateLocalUser (req, res, next) {
const { user: userService } = this.dtp.services;
try {
await userService.updateForAdmin(res.locals.userAccount, req.body);
await userService.updateLocalForAdmin(res.locals.userAccount, req.body);
res.redirect('/admin/user');
} catch (error) {
return next(error);
}
}
async getUserView (req, res, next) {
async getLocalUserView (req, res, next) {
const { comment: commentService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
@ -68,7 +67,7 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.userAccounts = await userService.getUserAccounts(res.locals.pagination, req.query.u);
res.locals.userAccounts = await userService.searchLocalUserAccounts(res.locals.pagination, req.query.u);
res.locals.totalUserCount = await userService.getTotalCount();
res.render('admin/user/index');
} catch (error) {

2
app/controllers/hive/user.js

@ -78,7 +78,7 @@ class HiveUserController extends SiteController {
throw new SiteError(406, 'Must include search term');
}
res.locals.q = await userService.filterUsername(req.query.q);
res.locals.q = userService.filterUsername(req.query.q);
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.userProfiles = await userService.getUserAccounts(res.locals.pagination, res.locals.q);
res.locals.userProfiles = res.locals.userProfiles.map((user) => {

3
app/controllers/home.js

@ -65,7 +65,6 @@ class HomeController extends SiteController {
async getHome (req, res, next) {
const {
announcement: announcementService,
feed: feedService,
hive: hiveService,
oauth2: oauth2Service,
} = this.dtp.services;
@ -76,8 +75,6 @@ class HomeController extends SiteController {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.constellationTimeline = await hiveService.getConstellationTimeline(req.user, res.locals.pagination);
res.locals.newsfeed = await feedService.getNewsfeed();
res.render('index');
} catch (error) {
this.log.error('failed to render home view', { error });

108
app/controllers/user.js

@ -23,7 +23,11 @@ class UserController extends SiteController {
session: sessionService,
} = dtp.services;
const upload = this.createMulter();
const upload = this.createMulter('user', {
limits: {
fileSize: 1024 * 1000 * 5, // 5MB
},
});
const router = express.Router();
dtp.app.use('/user', router);
@ -33,12 +37,12 @@ class UserController extends SiteController {
const otpSetup = otpAuthService.middleware('Account', {
adminRequired: false,
otpRequired: true,
otpRedirectURL: async (req) => { return `/user/${req.user._id}`; },
otpRedirectURL: async (req) => { return `/user/${req.user.username}`; },
});
const otpMiddleware = otpAuthService.middleware('Account', {
adminRequired: false,
otpRequired: false,
otpRedirectURL: async (req) => { return `/user/${req.user._id}`; },
otpRedirectURL: async (req) => { return `/user/${req.user.username}`; },
});
router.use(
@ -60,8 +64,11 @@ class UserController extends SiteController {
return next();
}
router.param('userId', this.populateUser.bind(this));
router.param('coreUserId', this.populateCoreUser.bind(this));
router.param('localUsername', this.populateLocalUsername.bind(this));
router.param('coreUsername', this.populateCoreUsername.bind(this));
router.param('localUserId', this.populateLocalUserId.bind(this));
router.param('coreUserId', this.populateCoreUserId.bind(this));
router.post(
'/core/:coreUserId/settings',
@ -72,7 +79,7 @@ class UserController extends SiteController {
);
router.post(
'/:userId/profile-photo',
'/:localUserId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.postProfilePhoto),
checkProfileOwner,
upload.single('imageFile'),
@ -80,7 +87,7 @@ class UserController extends SiteController {
);
router.post(
'/:userId/settings',
'/:localUserId/settings',
limiterService.createMiddleware(limiterService.config.user.postUpdateSettings),
checkProfileOwner,
upload.none(),
@ -123,7 +130,7 @@ class UserController extends SiteController {
);
router.get(
'/:userId/settings',
'/:localUsername/settings',
limiterService.createMiddleware(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
@ -131,7 +138,7 @@ class UserController extends SiteController {
this.getUserSettingsView.bind(this),
);
router.get(
'/:userId',
'/:localUsername',
limiterService.createMiddleware(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
@ -147,34 +154,84 @@ class UserController extends SiteController {
);
}
async populateUser (req, res, next, userId) {
async populateCoreUsername (req, res, next, coreUsername) {
const { user: userService } = this.dtp.services;
try {
userId = mongoose.Types.ObjectId(userId);
res.locals.username = userService.filterUsername(coreUsername);
res.locals.userProfileId = await userService.getCoreUserId(res.locals.username);
if (!res.locals.userProfileId) {
throw new SiteError(404, 'Core member not found');
}
// manually chain over to the ID parameter resolver
return this.populateCoreUserId(req, res, next, res.locals.userProfileId);
} catch (error) {
return next(new SiteError(406, 'Invalid User'));
this.log.error('failed to populate core username', { coreUsername, error });
return next(error);
}
}
async populateCoreUserId (req, res, next, coreUserId) {
const { user: userService } = this.dtp.services;
try {
res.locals.userProfile = await userService.getUserAccount(userId);
res.locals.userProfileId = mongoose.Types.ObjectId(coreUserId);
if (req.user && (req.user.type === 'CoreUser') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getCoreUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getCoreUserProfile(res.locals.userProfileId);
}
if (!res.locals.userProfile) {
throw new SiteError(404, 'Core member not found');
}
return next();
} catch (error) {
this.log.error('failed to populate userId', { userId, error });
this.log.error('failed to populate core user id', { coreUserId, error });
return next(error);
}
}
async populateCoreUser (req, res, next, coreUserId) {
const { coreNode: coreNodeService } = this.dtp.services;
async populateLocalUsername (req, res, next, username) {
const { user: userService } = this.dtp.services;
try {
coreUserId = mongoose.Types.ObjectId(coreUserId);
res.locals.username = userService.filterUsername(username);
res.locals.userProfileId = await userService.getLocalUserId(res.locals.username);
if (!res.locals.userProfileId) {
throw new SiteError(404, 'Local member not found');
}
if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
}
return next();
} catch (error) {
return next(new SiteError(406, 'Invalid User'));
this.log.error('failed to populate local username', { username, error });
return next(error);
}
}
async populateLocalUserId (req, res, next, userId) {
const { user: userService } = this.dtp.services;
try {
res.locals.userProfile = await coreNodeService.getUserByLocalId(coreUserId);
res.locals.userProfileId = mongoose.Types.ObjectId(userId);
if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
}
if (!res.locals.userProfile) {
throw new SiteError(404, 'Local member not found');
}
return next();
} catch (error) {
this.log.error('failed to populate coreUserId', { coreUserId, error });
this.log.error('failed to populate local user id', { userId, error });
return next(error);
}
}
@ -203,7 +260,7 @@ class UserController extends SiteController {
if (error) {
return next(error);
}
res.redirect(`/user/${res.locals.user._id}`);
res.redirect(`/user/${res.locals.user.username}`);
});
} catch (error) {
this.log.error('failed to create new user', { error });
@ -214,8 +271,9 @@ class UserController extends SiteController {
async postProfilePhoto (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('profile-photo');
await userService.updatePhoto(req.user, req.file);
const displayList = this.createDisplayList('profile-photo');
displayList.showNotification(
'Profile photo updated successfully.',
'success',
@ -235,8 +293,9 @@ class UserController extends SiteController {
async postHeaderImage (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('header-image');
await userService.updateHeaderImage(req.user, req.file);
const displayList = this.createDisplayList('header-image');
displayList.showNotification(
'Header image updated successfully.',
'success',
@ -256,10 +315,9 @@ class UserController extends SiteController {
async postUpdateCoreSettings (req, res) {
const { coreNode: coreNodeService } = this.dtp.services;
try {
const displayList = this.createDisplayList('app-settings');
await coreNodeService.updateUserSettings(req.user, req.body);
const displayList = this.createDisplayList('app-settings');
displayList.reload();
res.status(200).json({ success: true, displayList });
} catch (error) {

4
app/models/announcement.js

@ -29,4 +29,6 @@ const AnnouncementSchema = new Schema({
resourceStats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
});
module.exports = mongoose.model('Announcement', AnnouncementSchema);
module.exports = (conn) => {
return conn.model('Announcement', AnnouncementSchema);
};

4
app/models/attachment.js

@ -61,4 +61,6 @@ AttachmentSchema.index({
name: 'attachment_item_idx',
});
module.exports = mongoose.model('Attachment', AttachmentSchema);
module.exports = (conn) => {
return conn.model('Attachment', AttachmentSchema);
};

4
app/models/chat-message.js

@ -28,4 +28,6 @@ const ChatMessageSchema = new Schema({
attachments: { type: [Schema.ObjectId], ref: 'Attachment' },
});
module.exports = mongoose.model('ChatMessage', ChatMessageSchema);
module.exports = (conn) => {
return conn.model('ChatMessage', ChatMessageSchema);
};

4
app/models/chat-room-invite.js

@ -27,4 +27,6 @@ ChatRoomInviteSchema.index({
name: 'chatroom_invite_unique_idx',
});
module.exports = mongoose.model('ChatRoomInvite', ChatRoomInviteSchema);
module.exports = (conn) => {
return conn.model('ChatRoomInvite', ChatRoomInviteSchema);
};

4
app/models/chat-room.js

@ -41,4 +41,6 @@ ChatRoomSchema.index({
name: 'chatroom_public_open_idx',
});
module.exports = mongoose.model('ChatRoom', ChatRoomSchema);
module.exports = (conn) => {
return conn.model('ChatRoom', ChatRoomSchema);
};

4
app/models/comment.js

@ -72,4 +72,6 @@ CommentSchema.index({
name: 'comment_replies',
});
module.exports = mongoose.model('Comment', CommentSchema);
module.exports = (conn) => {
return conn.model('Comment', CommentSchema);
};

4
app/models/connect-token.js

@ -16,4 +16,6 @@ const ConnectTokenSchema = new Schema({
claimed: { type: Date },
});
module.exports = mongoose.model('ConnectToken', ConnectTokenSchema);
module.exports = (conn) => {
return conn.model('ConnectToken', ConnectTokenSchema);
};

4
app/models/content-report.js

@ -28,4 +28,6 @@ ContentReportSchema.index({
name: 'unique_user_content_report',
});
module.exports = mongoose.model('ContentReport', ContentReportSchema);
module.exports = (conn) => {
return conn.model('ContentReport', ContentReportSchema);
};

4
app/models/content-vote.js

@ -23,4 +23,6 @@ ContentVoteSchema.index({
name: 'unique_user_content_vote',
});
module.exports = mongoose.model('ContentVote', ContentVoteSchema);
module.exports = (conn) => {
return conn.model('ContentVote', ContentVoteSchema);
};

4
app/models/core-node-connect.js

@ -30,4 +30,6 @@ const CoreNodeConnectSchema = new Schema({
},
});
module.exports = mongoose.model('CoreNodeConnect', CoreNodeConnectSchema);
module.exports = (conn) => {
return conn.model('CoreNodeConnect', CoreNodeConnectSchema);
};

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

@ -35,4 +35,6 @@ const CoreNodeRequestSchema = new Schema({
},
});
module.exports = mongoose.model('CoreNodeRequest', CoreNodeRequestSchema);
module.exports = (conn) => {
return conn.model('CoreNodeRequest', CoreNodeRequestSchema);
};

4
app/models/core-node.js

@ -45,4 +45,6 @@ CoreNodeSchema.index({
name: 'core_address_idx',
});
module.exports = mongoose.model('CoreNode', CoreNodeSchema);
module.exports = (conn) => {
return conn.model('CoreNode', CoreNodeSchema);
};

4
app/models/core-user.js

@ -52,4 +52,6 @@ CoreUserSchema.index({
name: 'core_username_lc_unique',
});
module.exports = mongoose.model('CoreUser', CoreUserSchema);
module.exports = (conn) => {
return conn.model('CoreUser', CoreUserSchema);
};

4
app/models/csrf-token.js

@ -17,4 +17,6 @@ const CsrfTokenSchema = new Schema({
ip: { type: String, required: true },
});
module.exports = mongoose.model('CsrfToken', CsrfTokenSchema);
module.exports = (conn) => {
return conn.model('CsrfToken', CsrfTokenSchema);
};

4
app/models/email-blacklist.js

@ -31,4 +31,6 @@ EmailBlacklistSchema.index({
},
});
module.exports = mongoose.model('EmailBlacklist', EmailBlacklistSchema);
module.exports = (conn) => {
return conn.model('EmailBlacklist', EmailBlacklistSchema);
};

4
app/models/email-body.js

@ -12,4 +12,6 @@ const EmailBodySchema = new Schema({
body: { type: String, required: true },
});
module.exports = mongoose.model('EmailBody', EmailBodySchema);
module.exports = (conn) => {
return conn.model('EmailBody', EmailBodySchema);
};

4
app/models/email-log.js

@ -16,4 +16,6 @@ const EmailLogSchema = new Schema({
messageId: { type: String },
});
module.exports = mongoose.model('EmailLog', EmailLogSchema);
module.exports = (conn) => {
return conn.model('EmailLog', EmailLogSchema);
};

4
app/models/email-verify.js

@ -15,4 +15,6 @@ const EmailVerifySchema = new Schema({
token: { type: String, required: true },
});
module.exports = mongoose.model('EmailVerify', EmailVerifySchema);
module.exports = (conn) => {
return conn.model('EmailVerify', EmailVerifySchema);
};

4
app/models/email.js

@ -17,4 +17,6 @@ const EmailSchema = new Schema({
content: { type: Schema.ObjectId, required: true, index: true, refPath: 'contentType' },
});
module.exports = mongoose.model('Email', EmailSchema);
module.exports = (conn) => {
return conn.model('Email', EmailSchema);
};

4
app/models/emoji-reaction.js

@ -36,4 +36,6 @@ const EmojiReactionSchema = new Schema({
timestamp: { type: Number },
});
module.exports = mongoose.model('EmojiReaction', EmojiReactionSchema);
module.exports = (conn) => {
return conn.model('EmojiReaction', EmojiReactionSchema);
};

4
app/models/feed-entry.js

@ -23,4 +23,6 @@ FeedEntrySchema.index({
name: 'feed_entry_by_feed_idx',
});
module.exports = mongoose.model('FeedEntry', FeedEntrySchema);
module.exports = (conn) => {
return conn.model('FeedEntry', FeedEntrySchema);
};

4
app/models/feed.js

@ -18,4 +18,6 @@ const FeedSchema = new Schema({
published: { type: Date },
});
module.exports = mongoose.model('Feed', FeedSchema);
module.exports = (conn) => {
return conn.model('Feed', FeedSchema);
};

4
app/models/image.js

@ -32,4 +32,6 @@ const ImageSchema = new Schema({
},
});
module.exports = mongoose.model('Image', ImageSchema);
module.exports = (conn) => {
return conn.model('Image', ImageSchema);
};

4
app/models/kaleidoscope-event.js

@ -58,4 +58,6 @@ KaleidoscopeEventSchema.index({
name: 'evtsrc_site_author_index',
});
module.exports = mongoose.model('KaleidoscopeEvent', KaleidoscopeEventSchema);
module.exports = (conn) => {
return conn.model('KaleidoscopeEvent', KaleidoscopeEventSchema);
};

4
app/models/log.js

@ -29,4 +29,6 @@ const LogSchema = new Schema({
metadata: { type: Schema.Types.Mixed },
});
module.exports = mongoose.model('Log', LogSchema);
module.exports = (conn) => {
return conn.model('Log', LogSchema);
};

4
app/models/media-router.js

@ -50,4 +50,6 @@ const MediaRouterSchema = new Schema({
}
});
module.exports = mongoose.model('MediaRouter', MediaRouterSchema);
module.exports = (conn) => {
return conn.model('MediaRouter', MediaRouterSchema);
};

4
app/models/media-worker.js

@ -40,4 +40,6 @@ const MediaWorkerSchema = new Schema({
}
});
module.exports = mongoose.model('MediaWorker', MediaWorkerSchema);
module.exports = (conn) => {
return conn.model('MediaWorker', MediaWorkerSchema);
};

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

@ -72,4 +72,6 @@ const NetHostStatsSchema = new Schema({
network: { type: [NetworkInterfaceStatsSchema], required: true },
});
module.exports = mongoose.model('NetHostStats', NetHostStatsSchema);
module.exports = (conn) => {
return conn.model('NetHostStats', NetHostStatsSchema);
};

4
app/models/net-host.js

@ -42,4 +42,6 @@ const NetHostSchema = new Schema({
network: { type: [NetworkInterfaceSchema] },
});
module.exports = mongoose.model('NetHost', NetHostSchema);
module.exports = (conn) => {
return conn.model('NetHost', NetHostSchema);
};

4
app/models/newsletter-recipient.js

@ -18,4 +18,6 @@ const NewsletterRecipientSchema = new Schema({
},
});
module.exports = mongoose.model('NewsletterRecipient', NewsletterRecipientSchema);
module.exports = (conn) => {
return conn.model('NewsletterRecipient', NewsletterRecipientSchema);
};

4
app/models/newsletter.js

@ -28,4 +28,6 @@ const NewsletterSchema = new Schema({
},
});
module.exports = mongoose.model('Newsletter', NewsletterSchema);
module.exports = (conn) => {
return conn.model('Newsletter', NewsletterSchema);
};

4
app/models/oauth2-authorization-code.js

@ -16,4 +16,6 @@ const OAuth2AuthorizationCodeSchema = new Schema({
scopes: { type: [String], required: true },
});
module.exports = mongoose.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
module.exports = (conn) => {
return conn.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
};

4
app/models/oauth2-client.js

@ -40,4 +40,6 @@ OAuth2ClientSchema.index({
unique: true,
});
module.exports = mongoose.model('OAuth2Client', OAuth2ClientSchema);
module.exports = (conn) => {
return conn.model('OAuth2Client', OAuth2ClientSchema);
};

4
app/models/oauth2-token.js

@ -24,4 +24,6 @@ OAuth2TokenSchema.index({
name: 'oauth2_token_unique',
});
module.exports = mongoose.model('OAuth2Token', OAuth2TokenSchema);
module.exports = (conn) => {
return conn.model('OAuth2Token', OAuth2TokenSchema);
};

4
app/models/otp-account.js

@ -33,4 +33,6 @@ OtpAccountSchema.index({
name: 'otp_user_svc_uniq_idx',
});
module.exports = mongoose.model('OtpAccount', OtpAccountSchema);
module.exports = (conn) => {
return conn.model('OtpAccount', OtpAccountSchema);
};

4
app/models/resource-view.js

@ -29,4 +29,6 @@ ResourceViewSchema.index({
name: 'res_view_daily_unique',
});
module.exports = mongoose.model('ResourceView', ResourceViewSchema);
module.exports = (conn) => {
return conn.model('ResourceView', ResourceViewSchema);
};

4
app/models/resource-visit.js

@ -26,4 +26,6 @@ ResourceVisitSchema.index({
name: 'resource_visits_for_user',
});
module.exports = mongoose.model('ResourceVisit', ResourceVisitSchema);
module.exports = (conn) => {
return conn.model('ResourceVisit', ResourceVisitSchema);
};

4
app/models/sticker.js

@ -43,4 +43,6 @@ const StickerSchema = new Schema({
encoded: { type: StickerMediaSchema },
});
module.exports = mongoose.model('Sticker', StickerSchema);
module.exports = (conn) => {
return conn.model('Sticker', StickerSchema);
};

4
app/models/user-block.js

@ -14,4 +14,6 @@ const UserBlockSchema = new Schema({
blockedMembers: { type: [DtpUserSchema] },
});
module.exports = mongoose.model('UserBlock', UserBlockSchema);
module.exports = (conn) => {
return conn.model('UserBlock', UserBlockSchema);
};

4
app/models/user-notification.js

@ -24,4 +24,6 @@ const UserNotificationSchema = new Schema({
event: { type: Schema.ObjectId, required: true, ref: 'KaleidoscopeEvent' },
});
module.exports = mongoose.model('UserNotification', UserNotificationSchema);
module.exports = (conn) => {
return conn.model('UserNotification', UserNotificationSchema);
};

4
app/models/user-subscription.js

@ -24,4 +24,6 @@ const UserSubscriptionSchema = new Schema({
subscriptions: { type: [SubscriptionSchema] },
});
module.exports = mongoose.model('UserSubscription', UserSubscriptionSchema);
module.exports = (conn) => {
return conn.model('UserSubscription', UserSubscriptionSchema);
};

11
app/models/user.js

@ -22,17 +22,18 @@ const {
const UserSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1 },
email: { type: String, required: true, lowercase: true, unique: true },
email: { type: String, required: true, lowercase: true, unique: true, select: false },
username: { type: String, required: true },
username_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 },
passwordSalt: { type: String, required: true },
password: { type: String, required: true },
passwordSalt: { type: String, required: true, select: false },
password: { type: String, required: true, select: false },
displayName: { type: String },
bio: { type: String, maxlength: 300 },
picture: {
large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' },
},
header: { type: Schema.ObjectId, ref: 'Image' },
badges: { type: [String] },
flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false },
@ -42,4 +43,6 @@ const UserSchema = new Schema({
lastAnnouncement: { type: Date },
});
module.exports = mongoose.model('User', UserSchema);
module.exports = (conn) => {
return conn.model('User', UserSchema);
};

15
app/services/chat.js

@ -473,7 +473,12 @@ class ChatService extends SiteService {
async createMessage (author, messageDefinition) {
const { sticker: stickerService, user: userService } = this.dtp.services;
author = await userService.getUserAccount(author._id);
this.log.alert('user record', { author });
if (author.type === 'User') {
author = await userService.getLocalUserAccount(author._id);
} else {
author = await userService.getCoreUserAccount(author._id);
}
if (!author || !author.permissions || !author.permissions.canChat) {
throw new SiteError(403, `You are not permitted to chat at all on ${this.dtp.config.site.name}`);
}
@ -744,7 +749,13 @@ class ChatService extends SiteService {
const { user: userService } = this.dtp.services;
const NOW = new Date();
const userCheck = await userService.getUserAccount(user._id);
let userCheck;
if (user.type === '') {
userCheck = await userService.getLocalUserAccount(user._id);
} else {
userCheck = await userService.getCoreUserAccount(user._id);
}
if (!userCheck || !userCheck.permissions || !userCheck.permissions.canChat) {
throw new SiteError(403, 'You are not permitted to chat');
}

11
app/services/feed.js

@ -27,6 +27,17 @@ class FeedService extends SiteService {
this.jobQueue = await this.getJobQueue('newsroom', this.dtp.config.jobQueues.newsroom);
}
middleware (options) {
options = Object.assign({ maxEntryCount: 5 }, options);
return async (req, res, next) => {
if (this.isSystemRoute(req.path)) {
return next(); // don't load newsfeeds for non-content routes
}
res.locals.newsfeed = await this.getNewsfeed({ skip: 0, cpp: options.maxEntryCount });
return next();
};
}
async create (feedDefinition) {
feedDefinition.url = feedDefinition.url.trim();
const feedContent = await this.load(feedDefinition.url);

114
app/services/image.js

@ -16,7 +16,7 @@ const { SiteService, SiteAsync } = require('../../lib/site-lib');
class ImageService extends SiteService {
constructor (dtp) {
constructor(dtp) {
super(dtp, module.exports);
this.populateImage = [
{
@ -26,20 +26,20 @@ class ImageService extends SiteService {
];
}
async start ( ) {
async start() {
await super.start();
await fs.promises.mkdir(process.env.DTP_IMAGE_WORK_PATH, { recursive: true });
}
async create (owner, imageDefinition, file) {
async create(owner, imageDefinition, file) {
const NOW = new Date();
const { minio: minioService } = this.dtp.services;
try {
this.log.debug('processing uploaded image', { imageDefinition, file });
const sharpImage = await sharp(file.path);
const sharpImage = sharp(file.path);
const metadata = await sharpImage.metadata();
// create an Image model instance, but leave it here in application memory.
// we don't persist it to the db until MinIO accepts the binary data.
const image = new SiteImage();
@ -49,12 +49,12 @@ class ImageService extends SiteService {
image.size = file.size;
image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
image.metadata = this.makeImageMetadata(metadata);
const imageId = image._id.toString();
const ownerId = owner._id.toString();
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`;
image.file.key = fileKey;
// upload the image file to MinIO
const response = await minioService.uploadFile({
bucket: image.file.bucket,
@ -65,13 +65,13 @@ class ImageService extends SiteService {
'Content-Length': file.size,
},
});
// store the eTag from MinIO in the Image model
image.file.etag = response.etag;
// save the Image model to the db
await image.save();
this.log.info('processed uploaded image', { ownerId, imageId, fileKey });
return image.toObject();
} catch (error) {
@ -83,14 +83,14 @@ class ImageService extends SiteService {
}
}
async getImageById (imageId) {
async getImageById(imageId) {
const image = await SiteImage
.findById(imageId)
.populate(this.populateImage);
return image;
}
async getRecentImagesForOwner (owner) {
async getRecentImagesForOwner(owner) {
const images = await SiteImage
.find({ owner: owner._id })
.sort({ created: -1 })
@ -100,7 +100,7 @@ class ImageService extends SiteService {
return images;
}
async deleteImage (image) {
async deleteImage(image) {
const { minio: minioService } = this.dtp.services;
this.log.debug('removing image from storage', { bucket: image.file.bucket, key: image.file.key });
@ -110,13 +110,13 @@ class ImageService extends SiteService {
await SiteImage.deleteOne({ _id: image._id });
}
async processImageFile (owner, file, outputs, options) {
async processImageFile(owner, file, outputs, options) {
this.log.debug('processing image file', { owner, file, outputs });
const sharpImage = sharp(file.path);
return this.processImage(owner, sharpImage, outputs, options);
}
async processImage (owner, sharpImage, outputs, options) {
async processImage(owner, sharpImage, outputs, options) {
const NOW = new Date();
const service = this;
const { minio: minioService } = this.dtp.services;
@ -128,7 +128,7 @@ class ImageService extends SiteService {
const imageWorkPath = process.env.DTP_IMAGE_WORK_PATH || '/tmp';
const metadata = await sharpImage.metadata();
async function processOutputImage (output) {
async function processOutputImage(output) {
const outputMetadata = service.makeImageMetadata(metadata);
outputMetadata.width = output.width;
outputMetadata.height = output.height;
@ -149,7 +149,7 @@ class ImageService extends SiteService {
height: output.height,
options: output.resizeOptions,
})
;
;
chain = chain[output.format](output.formatParameters);
output.filePath = path.join(imageWorkPath, `${image._id}.${output.width}x${output.height}.${output.format}`);
@ -165,11 +165,11 @@ class ImageService extends SiteService {
const imageId = image._id.toString();
const ownerId = owner._id.toString();
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/images/${imageId.slice(0, 3)}/${imageId}.${output.format}`;
image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
image.file.key = fileKey;
image.size = output.stat.size;
// upload the image file to MinIO
const response = await minioService.uploadFile({
bucket: image.file.bucket,
@ -180,13 +180,13 @@ class ImageService extends SiteService {
'Content-Length': output.stat.size,
},
});
// store the eTag from MinIO in the Image model
image.file.etag = response.etag;
// save the Image model to the db
await image.save();
service.log.info('processed uploaded image', { ownerId, imageId, fileKey });
if (options.removeWorkFiles) {
@ -216,7 +216,73 @@ class ImageService extends SiteService {
await SiteAsync.each(outputs, processOutputImage, 4);
}
makeImageMetadata (metadata) {
async getSiteIconInfo() {
const siteDomain = this.dtp.config.site.domainKey;
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);
let icon;
try {
await fs.promises.access(siteIconDir);
const iconMetadata = await sharp(path.join(siteIconDir, 'icon-512x512.png')).metadata();
icon = {
metadata: iconMetadata,
path: `/img/icon/${siteDomain}/icon-512x512.png`,
};
} catch (error) {
icon = null;
}
return icon;
}
async updateSiteIcon(imageDefinition, file) {
this.log.debug('updating site icon', { imageDefinition, file });
try {
const siteDomain = this.dtp.config.site.domainKey;
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);
const sourceIconFilePath = file.path;
const sizes = [16, 32, 36, 48, 57, 60, 70, 72, 76, 96, 114, 120, 144, 150, 152, 180, 192, 256, 310, 384, 512];
await fs.promises.mkdir(siteIconDir, { force: true, recursive: true });
for (var size of sizes) {
await sharp(sourceIconFilePath).resize({
fit: sharp.fit.contain,
width: size,
height: size,
}).png()
.toFile(path.join(siteIconDir, `icon-${size}x${size}.png`));
}
await fs.promises.cp(sourceIconFilePath, path.join(siteIconDir, `${siteDomain}.png`));
await fs.promises.cp(sourceIconFilePath, path.join(siteImagesDir, 'social-cards', `${siteDomain}.png`));
return path.join(siteIconDir, 'icon-512x512.png');
} catch (error) {
this.log.error('failed to update site icon', { error });
throw error;
} finally {
this.log.info('removing uploaded image from local file system', { file: file.path });
await fs.promises.rm(file.path);
}
}
makeImageMetadata(metadata) {
return {
format: metadata.format,
size: metadata.size,

16
app/services/otp-auth.js

@ -217,8 +217,20 @@ class OtpAuthService extends SiteService {
return true;
}
async removeForUser (user) {
return await OtpAccount.deleteMany({ user: user });
async destroyOtpSession (req, serviceName) {
delete req.session.otp[serviceName];
await this.saveSession(req);
}
async removeForUser (user, serviceName) {
return await OtpAccount.findOneAndDelete({ user: user, service: serviceName });
}
async getBackupTokens (user, serviceName) {
const tokens = await OtpAccount.findOne({ user: user._id, service: serviceName })
.select('+backupTokens')
.lean();
return tokens.backupTokens;
}
}

3
app/services/session.js

@ -92,8 +92,9 @@ class SessionService extends SiteService {
delete user.stats._id;
delete user.optIn._id;
break;
case 'local':
user = await userService.getUserAccount(userId);
user = await userService.getLocalUserAccount(userId);
user.type = 'User';
break;
}

189
app/services/user.js

@ -20,6 +20,12 @@ const uuidv4 = require('uuid').v4;
const { SiteError, SiteService } = require('../../lib/site-lib');
/*
* The entire concept of "get a user" is in flux right now. It's best to just
* ignore what's happening in this service right now, and focus on other
* features in the sytem.
*/
class UserService extends SiteService {
constructor (dtp) {
@ -100,21 +106,21 @@ class UserService extends SiteService {
user.password = maskedPassword;
user.flags = {
isAdmin: userDefinition.isAdmin || false,
isModerator: userDefinition.isModerator || false,
isEmailVerified: userDefinition.isEmailVerified || false,
isAdmin: false,
isModerator: false,
isEmailVerified: false,
};
user.permissions = {
canLogin: userDefinition.canLogin || true,
canChat: userDefinition.canChat || true,
canComment: userDefinition.canComment || true,
canReport: userDefinition.canReport || true,
canLogin: true,
canChat: true,
canComment: true,
canReport: true,
};
user.optIn = {
system: userDefinition.optInSystem || true,
marketing: userDefinition.optInMarketing || false,
system: true,
marketing: false,
};
this.log.info('creating new user account', { email: userDefinition.email });
@ -174,7 +180,7 @@ class UserService extends SiteService {
async emailOptOut (userId, category) {
userId = mongoose.Types.ObjectId(userId);
const user = await this.getUserAccount(userId);
const user = await this.getLocalUserAccount(userId);
if (!user) {
throw new SiteError(406, 'Invalid opt-out token');
}
@ -199,7 +205,6 @@ class UserService extends SiteService {
throw SiteError(403, 'Invalid user account operation');
}
// strip characters we don't want to allow in username
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -220,8 +225,7 @@ class UserService extends SiteService {
);
}
async updateForAdmin (user, userDefinition) {
// strip characters we don't want to allow in username
async updateLocalForAdmin (user, userDefinition) {
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -265,7 +269,6 @@ class UserService extends SiteService {
const updateOp = { $set: { }, $unset: { } };
// strip characters we don't want to allow in username
updateOp.$set.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
if (!updateOp.$set.username || (updateOp.$set.username.length === 0)) {
throw new SiteError(400, 'Must include a username');
@ -303,7 +306,7 @@ class UserService extends SiteService {
}, options);
const accountEmail = account.username.trim().toLowerCase();
const accountUsername = await this.filterUsername(accountEmail);
const accountUsername = this.filterUsername(accountEmail);
this.log.debug('locating user record', { accountEmail, accountUsername });
const user = await User
@ -393,28 +396,23 @@ class UserService extends SiteService {
);
}
filterUserObject (user) {
const filteredUser = {
_id: user._id,
created: user.created,
displayName: user.displayName,
username: user.username,
username_lc: user.username_lc,
bio: user.bio,
flags: user.flags,
permissions: user.permissions,
picture: user.picture,
};
if (filteredUser.flags && filteredUser.flags._id) {
delete filteredUser.flags._id;
async getLocalUserId (username) {
const user = await User.findOne({ username_lc: username }).select('_id').lean();
if (!user) {
return; // undefined
}
if (filteredUser.permissions && filteredUser.permissions._id) {
delete filteredUser.permissions._id;
return user._id;
}
async getCoreUserId (username) {
const user = await CoreUser.findOne({ username_lc: username }).select('_id').lean();
if (!user) {
return; // undefined
}
return filteredUser;
return user._id;
}
async getUserAccount (userId) {
async getLocalUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +optIn +picture')
@ -427,11 +425,47 @@ class UserService extends SiteService {
return user;
}
async getUserAccounts (pagination, username) {
async getCoreUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +optIn +picture')
.populate(this.populateUser)
.lean();
if (!user) {
throw new SiteError(404, 'Core member account not found');
}
user.type = 'CoreUser';
return user;
}
async getLocalUserProfile (userId) {
const user = await User
.findById(userId)
.select('+email +flags +settings')
.populate(this.populateUser)
.lean();
user.type = 'User';
return user;
}
async getCoreUserProfile (userId) {
const user = await CoreUser
.findById(userId)
.select('+core +flags +settings')
.populate(this.populateUser)
.lean();
user.type = 'CoreUser';
return user;
}
async searchLocalUserAccounts (pagination, username) {
let search = { };
if (username) {
username = this.filterUsername(username);
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
const users = await User
.find(search)
.sort({ username_lc: 1 })
@ -496,6 +530,26 @@ class UserService extends SiteService {
return user;
}
async searchCoreUserAccounts (pagination, username) {
let search = { };
username = this.filterUsername(username);
if (username) {
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
const users = await CoreUser
.find(search)
.sort({ username_lc: 1 })
.select('+core +coreUserId +flags +permissions +optIn')
.skip(pagination.skip)
.limit(pagination.cpp)
.lean()
;
return users.map((user) => { user.type = 'CoreUser'; return user; });
}
async getRecent (maxCount = 3) {
const users = User
.find()
@ -575,10 +629,6 @@ class UserService extends SiteService {
return actions;
}
async filterUsername (username) {
return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
}
async checkUsername (username) {
if (!username || (typeof username !== 'string') || (username.length === 0)) {
throw new SiteError(406, 'Invalid username');
@ -594,6 +644,34 @@ class UserService extends SiteService {
}
}
filterUsername (username) {
while (username[0] === '@') {
username = username.slice(1);
}
return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
}
filterUserObject (user) {
const filteredUser = {
_id: user._id,
created: user.created,
displayName: user.displayName,
username: user.username,
username_lc: user.username_lc,
bio: user.bio,
flags: user.flags,
permissions: user.permissions,
picture: user.picture,
};
if (filteredUser.flags && filteredUser.flags._id) {
delete filteredUser.flags._id;
}
if (filteredUser.permissions && filteredUser.permissions._id) {
delete filteredUser.permissions._id;
}
return filteredUser;
}
async recordProfileView (user, req) {
const { resource: resourceService } = this.dtp.services;
await resourceService.recordView(req, 'User', user._id);
@ -649,6 +727,41 @@ class UserService extends SiteService {
await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } });
}
async updateHeaderImage (user, file) {
const { image: imageService } = this.dtp.services;
await this.removeHeaderImage(user.header);
const images = [
{
width: 1400,
height: 400,
format: 'jpeg',
formatParameters: {
quality: 80,
},
},
];
await imageService.processImageFile(user, file, images);
await User.updateOne(
{ _id: user._id },
{
$set: {
'header': images[0].image._id,
},
},
);
}
async removeHeaderImage (user) {
const { image: imageService } = this.dtp.services;
user = await this.getUserAccount(user._id);
if (user.header) {
await imageService.deleteImage(user.header);
}
await User.updateOne({ _id: user._id }, { $unset: { 'header': '' } });
}
async blockUser (user, blockedUser) {
if (user._id.equals(blockedUser._id)) {
throw new SiteError(406, "You can't block yourself");

51
app/views/admin/components/file-upload-image.pug

@ -0,0 +1,51 @@
mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaultImage, currentImage, cropperOptions)
div(id= containerId).dtp-file-upload
form(method="POST", action= actionUrl, enctype="multipart/form-data", onsubmit= "return dtp.app.submitImageForm(event);").uk-form
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-body
div(uk-grid).uk-flex-middle.uk-flex-center
div(class="uk-width-1-1 uk-width-auto@m")
.upload-image-container.size-512
if !!currentImage
img(id= imageId, src= currentImage.path, class= imageClass).sb-large
else
img(id= imageId, src= defaultImage, class= imageClass)
div(class="uk-width-1-1 uk-width-auto@m")
.uk-text-small.uk-margin
#file-select
.uk-margin(class="uk-text-center uk-text-left@m")
span.uk-text-middle Select an image
div(uk-form-custom).uk-margin-small-left
input(
type="file",
formenctype="multipart/form-data",
accept=".jpg,.png,image/jpeg,image/png",
data-file-select-container= containerId,
data-file-select="test-image-upload",
data-file-size-element= "file-size",
data-file-max-size= 15 * 1024000,
data-image-id= imageId,
data-cropper-options= cropperOptions,
onchange="return dtp.app.selectImageFile(event);",
)
button(type="button", tabindex="-1").uk-button.uk-button-default Select
#file-info(class="uk-text-center uk-text-left@m", hidden)
#file-name.uk-text-bold
if currentImage
div resolution: #[span#image-resolution-w= numeral(currentImage.metadata.width).format('0,0')]x#[span#image-resolution-h= numeral(currentImage.metadata.height).format('0,0')]
div size: #[span#file-size= numeral(currentImage.metadata.size).format('0,0.00b')]
div last modified: #[span#file-modified= moment(currentImage.created).format('MMM DD, YYYY')]
else
div resolution: #[span#image-resolution-w 512]x#[span#image-resolution-h 512]
div size: #[span#file-size N/A]
div last modified: #[span#file-modified N/A]
.uk-card-footer
div(class="uk-flex-center", uk-grid)
#file-save-btn(hidden).uk-width-auto
button(
type="submit",
).uk-button.uk-button-primary Save

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

@ -12,6 +12,18 @@ ul(uk-nav).uk-nav-default
span.nav-item-icon
i.fas.fa-cog
span.uk-margin-small-left Settings
li(class={ 'uk-active': (adminView === 'image-settings') })
a(href="/admin/settings/images")
span.nav-item-icon
i.fas.fa-image
span.uk-margin-small-left Image Settings
li(class={ 'uk-active': (adminView === 'otp') })
a(href="/admin/otp")
span.nav-item-icon
i.fas.fa-cog
span.uk-margin-small-left Otp Settings
li.uk-nav-divider

2
app/views/admin/core-user/form.pug

@ -13,7 +13,7 @@ block content
if userAccount.displayName
.uk-text-large= userAccount.displayName
div
a(href=`/user/${userAccount._id}`) @#{userAccount.username}
a(href=`/user/${userAccount.username}`) @#{userAccount.username}
.uk-card-body
.uk-margin

7
app/views/admin/layouts/main.pug

@ -4,11 +4,6 @@ block vendorcss
block content-container
block page-header
section.uk-section.uk-section-header.uk-section-xsmall
.uk-container
h1.uk-text-center.uk-margin-remove #{site.name} Admin
block admin-layout
section.uk-section.uk-section-default.uk-section-small
@ -21,4 +16,4 @@ block content-container
include ../components/menu
div(class="uk-width-1-1 uk-flex-first uk-width-expand@m").uk-width-expand
block content
block content

28
app/views/admin/otp/index.pug

@ -0,0 +1,28 @@
extends ../layouts/main
block content
div(uk-grid).uk-flex-middle
.uk-width-expand
h1.margin-remove Tokens
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container
.uk-text-small
h4 This is where you will regenerate OTP tokens for your admin account and destroy your old OTP account.
//- .uk-width-auto
button(
type="button",
data-user= user._id,
onclick="return dtp.adminApp.generateOTPTokens(event);",
).uk-button.dtp-button-danger
+renderButtonIcon('fa-repeat', 'Generate OTP Tokens')
//- regenerate route should set this so tokens can be viewed once.
if otpRegen
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container
h3 You should save these tokens in a safe place. This is the only time you will see them.
p These tokens should be saved in a safe place so you can get into your account should you lose your 2FA device
each token of tokens
ul.uk-list.uk-list-divider
li
.uk-text-small= token.token

28
app/views/admin/settings/images.pug

@ -0,0 +1,28 @@
extends ../layouts/main
block vendorcss
link(rel='stylesheet', href=`/cropperjs/cropper.min.css?v=${pkg.version}`)
block vendorjs
script(src=`/cropperjs/cropper.min.js?v=${pkg.version}`)
block content
include ../components/file-upload-image
//- h2 Add or replace your site images here
div(uk-grid).uk-flex-middle
.uk-width-expand
fieldset
legend Site Icon
.uk-margin
if siteIcon
p.uk-card-title Replace your site icon below.
else
p.uk-card-title You do not currently have a site icon. Add one below.
+renderFileUploadImage(
`/admin/settings/images/updateSiteIcon`,
'site-icon-upload',
'site-icon-file',
'site-icon-picture',
`/img/icon/dtp-base.png`,
siteIcon,
{ aspectRatio: 1 },
)

4
app/views/admin/user/form.pug

@ -5,7 +5,7 @@ block content
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-2-3@l")
form(method="POST", action=`/admin/user/${userAccount._id}`).uk-form
form(method="POST", action=`/admin/user/local/${userAccount._id}`).uk-form
input(type="hidden", name="username", value= userAccount.username)
input(type="hidden", name="displayName", value= userAccount.displayName)
.uk-card.uk-card-default.uk-card-small
@ -20,7 +20,7 @@ block content
.uk-width-auto
a(href=`mailto:${userAccount.email}`)= userAccount.email
.uk-width-auto
a(href=`/user/${userAccount._id}`) @#{userAccount.username}
a(href=`/user/${userAccount.username}`) @#{userAccount.username}
.uk-card-body
.uk-margin

4
app/views/admin/user/index.pug

@ -22,10 +22,10 @@ block content
each userAccount in userAccounts
tr
td
a(href=`/admin/user/${userAccount._id}`)= userAccount.username
a(href=`/admin/user/local/${userAccount._id}`)= userAccount.username
td
if userAccount.displayName
a(href=`/admin/user/${userAccount._id}`)= userAccount.displayName
a(href=`/admin/user/local/${userAccount._id}`)= userAccount.displayName
else
.uk-text-muted N/A
td= moment(userAccount.created).format('YYYY-MM-DD hh:mm a')

8
app/views/components/library.pug

@ -40,9 +40,9 @@ include section-title
function getUserProfileUrl (user) {
if (user.core) {
return `/user/core/${user._id}`;
return `/user/core/${user.username}`;
}
return `/user/${user._id}`;
return `/user/${user.username}`;
}
mixin renderCell (label, value, className)
@ -60,6 +60,6 @@ mixin renderBackButton (options)
mixin renderUserLink (user)
if user.coreUserId
a(href=`/user/core/${user._id}`)= `${user.username}@${user.core.meta.domainKey}`
a(href=`/user/core/${user.username}`)= `${user.username}@${user.core.meta.domainKey}`
else
a(href=`/user/${user._id}`)= user.displayName || user.username
a(href=`/user/${user.username}`)= user.displayName || user.username

4
app/views/components/navbar.pug

@ -47,12 +47,12 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
li.uk-nav-heading.uk-text-center= user.displayName || user.username
li.uk-nav-divider
li
a(href= user.core ? `/user/core/${user._id}` : `/user/${user._id}`)
a(href= user.core ? `/user/core/${user.username}` : `/user/${user.username}`)
span.nav-item-icon
i.fas.fa-user
span Profile
li
a(href= user.core ? `/user/core/${user._id}/settings` : `/user/${user._id}/settings`)
a(href= user.core ? `/user/core/${user.username}/settings` : `/user/${user.username}/settings`)
span.nav-item-icon
i.fas.fa-cog
span Settings

4
app/views/components/off-canvas.pug

@ -40,7 +40,7 @@ mixin renderMenuItem (iconClass, label)
.uk-width-expand Chat
li(class={ "uk-active": (currentView === 'user-settings') })
a(href=`/user/${user._id}`).uk-display-block
a(href=`/user/${user.username}`).uk-display-block
div(uk-grid).uk-grid-collapse
.uk-width-auto
.app-menu-icon
@ -48,7 +48,7 @@ mixin renderMenuItem (iconClass, label)
.uk-width-expand Profile
li(class={ "uk-active": (currentView === 'user-settings') })
a(href=`/user/${user._id}/settings`).uk-display-block
a(href=`/user/${user.username}/settings`).uk-display-block
div(uk-grid).uk-grid-collapse
.uk-width-auto
.app-menu-icon

2
app/views/layouts/main-sidebar.pug

@ -1,6 +1,8 @@
extends main
block content-container
block content-header
section.uk-section.uk-section-default.uk-section-small
.uk-container
div(uk-grid)#dtp-content-grid

10
app/views/newsroom/components/feed-entry-list-item.pug

@ -1,9 +1,9 @@
mixin renderNewsroomFeedEntryListItem (entry)
.uk-text-bold
a(href= entry.link, target="_blank").uk-link-reset= entry.title
.uk-text-small
div(uk-grid).uk-grid-small
.uk-width-expand
a(href= entry.link, target="_blank").dtp-link
div= entry.title
.uk-article-meta
div(uk-grid).uk-grid-small.uk-grid-divider
.uk-width-auto.uk-text-truncate
a(href= entry.feed.link, target="_blank").uk-link-reset= entry.feed.title
.uk-width-auto
div= moment(entry.published).fromNow()

15
app/views/otp/new-account.pug

@ -5,7 +5,18 @@ block content
.uk-container
h1 2FA Setup Successful
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container
h3 You should save these tokens in a safe place. This is the only time you will see them.
p These tokens should be saved in a safe place so you can get into your account should you lose your 2FA device
each token of otpAccount.backupTokens
ul.uk-list.uk-list-divider
li
.uk-text-small= token.token
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container
p Your account is now enabled with access to #{site.name} #{otpServiceName}.
a(href= otpRedirectURL, title="Continue").uk-button.uk-button-primary.uk-border-pill Continue
p Your account is now enabled with access to #{site.name} #{otpAccount.service}.
a(href= otpRedirectURL, title="Continue").uk-button.uk-button-primary.uk-border-pill Continue

2
app/views/otp/welcome.pug

@ -41,7 +41,7 @@ block content
.uk-margin
button(type="submit").uk-button.dtp-button-primary.uk-border-pill Enable 2FA
div(class="uk-width-1-1 uk-text-center uk-text-left@m", hidden)
div(class="uk-width-1-1 uk-text-center uk-text-left@m")
.uk-margin
p Or, if your authenticator doesn't support scanning QR codes, you can enter the OTP configuration information shown here to begin displaying codes:
pre(

48
app/views/welcome/core-home.pug

@ -3,26 +3,34 @@ block content
section.uk-section.uk-section-default
.uk-container
.uk-card.uk-card-default
.uk-card-header
h1.uk-card-title Select Community
.uk-card-body
div(uk-grid).uk-grid-small
each core in connectedCores
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
//- pre= JSON.stringify(connectedCores, null, 2)
a(href=`/auth/core/${core._id}`).uk-display-block.uk-link-reset
.dtp-core-list-item.uk-border-rounded
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-auto
img(src=`http://${core.meta.domain}/img/icon/dtp-core.svg`, style="width: 48px; height: auto;")
.uk-width-expand
.core-name= core.meta.name
.core-description= core.meta.description
if Array.isArray(hosts) && (hosts.length > 0)
.uk-card.uk-card-default
.uk-card-header
h1.uk-card-title Select Community
.uk-card-body
div(uk-grid).uk-grid-small
each core in connectedCores
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
//- pre= JSON.stringify(connectedCores, null, 2)
a(href=`/auth/core/${core._id}`).uk-display-block.uk-link-reset
.dtp-core-list-item.uk-border-rounded
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-auto
img(src=`http://${core.meta.domain}/img/icon/dtp-core.svg`, style="width: 48px; height: auto;")
.uk-width-expand
.core-name= core.meta.name
.core-description= core.meta.description
.uk-card-footer
div(uk-grid).uk-grid-small
.uk-width-expand
+renderBackButton()
+renderBackButton()
else
.uk-card.uk-card-default
.uk-card-header
h1.uk-card-title There are no communities connected to this site
.uk-card-footer
div(uk-grid).uk-grid-small
.uk-width-expand
+renderBackButton()

37
client/js/site-admin-app.js

@ -415,6 +415,43 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
return false;
}
async submitImageForm (event) {
event.preventDefault();
event.stopPropagation();
const formElement = event.currentTarget || event.target;
const form = new FormData(formElement);
this.cropper.getCroppedCanvas().toBlob(async (imageData) => {
try {
form.append('imageFile', imageData, 'icon.png');
this.log.info('submitImageForm', 'updating site image', { event, action: formElement.action });
const response = await fetch(formElement.action, {
method: formElement.method,
body: form,
});
if (!response.ok) {
let json;
try {
json = await response.json();
} catch (error) {
throw new Error('Server error');
}
throw new Error(json.message || 'Server error');
}
await this.processResponse(response);
window.location.reload();
} catch (error) {
UIkit.modal.alert(`Failed to update site image: ${error.message}`);
}
});
return;
}
}
dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp;

7
client/less/site/main.less

@ -50,6 +50,13 @@ body {
}
}
a.dtp-link {
color: inherit;
&:hover {
color: @global-link-hover-color;
}
}
.dtp-site-footer {
.uk-subnav {
a.dtp-social-link {

9
client/less/site/markdown.less

@ -0,0 +1,9 @@
.markdown-block {
font-size: @global-font-size;
line-height: @global-line-height;
color: @global-color;
p:last-of-type {
margin-bottom: 0;
}
}

7
client/less/style.common.less

@ -10,14 +10,15 @@
@import "site/kaleidoscope-event.less";
@import "site/nav.less";
@import "site/button.less";
@import "site/content.less";
@import "site/core-node.less";
@import "site/dashboard.less";
@import "site/site.less";
@import "site/form.less";
@import "site/button.less";
@import "site/sidebar.less";
@import "site/markdown.less";
@import "site/section.less";
@import "site/sidebar.less";
@import "site/site.less";
@import "site/chat.less";
@import "site/kaleidoscope-event.less";

4
dtp-webapp-cli.js

@ -34,6 +34,10 @@ module.grantPermission = async (target, permission) => {
const User = mongoose.model('User');
try {
const user = await User.findOne({ email: target }).select('+permissions +flags');
if (!user) {
throw new Error(`User not found (email: ${target})`);
}
switch (permission) {
case 'admin':
user.flags.isAdmin = true;

10
dtp-webapp.js

@ -21,12 +21,16 @@ module.config = {
site: require(path.join(module.rootPath, 'config', 'site')),
http: require(path.join(module.rootPath, 'config', 'http')),
https: require(path.join(module.rootPath, 'config', 'https')),
registerMiddleware: async (dtp, app) => {
const { feed: feedService } = dtp.services;
app.use(feedService.middleware({ maxEntryCount: 5 }));
},
};
module.log = new SiteLog(module, module.config.component);
module.shutdown = async ( ) => {
return await SitePlatform.shutdown();
};
(async ( ) => {
@ -46,8 +50,8 @@ module.shutdown = async ( ) => {
process.once('SIGINT', async ( ) => {
module.log.info('SIGINT received');
module.log.info('requesting shutdown...');
await module.shutdown();
const exitCode = await SitePlatform.shutdown();
const exitCode = await module.shutdown();
process.nextTick(( ) => {
process.exit(exitCode);
});

2
lib/client/js/dtp-log.js

@ -1,4 +1,4 @@
// dtpweb-log.js
// dtp-log.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

2
lib/client/js/dtp-socket.js

@ -1,4 +1,4 @@
// dtpweb-socket.js
// dtp-socket.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

7
lib/site-common.js

@ -106,6 +106,13 @@ class SiteCommon extends EventEmitter2 {
});
}
isSystemRoute (pathname) {
return pathname.startsWith('/auth') ||
pathname.startsWith('/image') ||
pathname.startsWith('/manifest')
;
}
isValidString (text) {
return text && (typeof text === 'string') && (text.length > 0);
}

10
lib/site-ioserver.js

@ -15,12 +15,12 @@ const ConnectToken = mongoose.model('ConnectToken');
const marked = require('marked');
const { SiteLog } = require(path.join(__dirname, 'site-log'));
const { SiteCommon } = require(path.join(__dirname, 'site-common'));
const Events = require('events');
class SiteIoServer extends Events {
class SiteIoServer extends SiteCommon {
constructor (dtp) {
super();
super(dtp, { name: 'ioServer', slug: 'io-server' });
this.dtp = dtp;
this.log = new SiteLog(dtp, DTP_COMPONENT);
}
@ -74,6 +74,10 @@ class SiteIoServer extends Events {
}
async stop ( ) {
if (this.io) {
this.io.close();
delete this.io;
}
}

22
lib/site-platform.js

@ -34,12 +34,22 @@ module.connectDatabase = async (/*dtp*/) => {
host: process.env.MONGODB_HOST,
database: process.env.MONGODB_DATABASE,
});
const mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`;
const mongoConnectionInfo = {
host: process.env.MONGODB_HOST,
db: process.env.MONGODB_DATABASE,
username: encodeURIComponent(process.env.MONGODB_USERNAME),
password: encodeURIComponent(process.env.MONGODB_PASSWORD),
options: process.env.MONGODB_OPTIONS || '',
};
let mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`;
if (process.env.NODE_ENV === 'production'){
mongoConnectUri = `mongodb://${mongoConnectionInfo.username}:${mongoConnectionInfo.password}@${mongoConnectionInfo.host}/${mongoConnectionInfo.options}`;
}
module.db = await mongoose.connect(mongoConnectUri, {
socketTimeoutMS: 0,
keepAlive: true,
keepAliveInitialDelay: 300000,
dbName: process.env.MONGODB_DATABASE,
dbName: mongoConnectionInfo.db,
});
module.log.info('connected to MongoDB');
} catch (error) {
@ -48,12 +58,12 @@ module.connectDatabase = async (/*dtp*/) => {
}
};
module.loadModels = async (dtp) => {
dtp.models = module.models = [ ];
const modelScripts = glob.sync(path.join(dtp.config.root, 'app', 'models', '*.js'));
modelScripts.forEach((modelScript) => {
const model = require(modelScript);
const instance = require(modelScript);
const model = instance(module.db);
if (module.models[model.modelName]) {
module.log.error('model name collision', { name: model.modelName });
process.exit(-1);
@ -146,7 +156,7 @@ module.loadControllers = async (dtp) => {
await SiteAsync.each(scripts, async (script) => {
const controller = await require(script);
controller.instance = await controller.create(dtp);
module.log.info('controller loaded', { name: controller.name, slug: controller.slug });
dtp.controllers[controller.name] = controller;
inits.push(controller);
@ -196,7 +206,7 @@ module.exports.startPlatform = async (dtp) => {
await module.connectRedis(dtp);
await module.loadModels(dtp);
SiteLog.setModel(mongoose.model('Log'));
SiteLog.setModel(module.db.model('Log'));
await module.loadServices(dtp);

2
package.json

@ -110,4 +110,4 @@
"webpack-stream": "^7.0.0",
"workbox-webpack-plugin": "^6.5.0"
}
}
}
Loading…
Cancel
Save