Merge branch 'master' of git.digitaltelepresence.com:digital-telepresence/dtp-sites

master
rob 1 year ago
commit 60020dc637

1
.gitignore vendored

@ -4,7 +4,6 @@ ssl/*crt
ssl/*key
data/minio
data/minio.old
node_modules
dist
start-local-*

@ -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('/page', await this.loadChild(path.join(__dirname, 'admin', 'page')));
router.use('/post', await this.loadChild(path.join(__dirname, 'admin', 'post')));
router.use('/settings', await this.loadChild(path.join(__dirname, 'admin', 'settings')));
@ -89,6 +90,7 @@ class AdminController extends SiteController {
};
res.locals.channels = await venueService.getChannels();
res.locals.pageTitle = `Admin Dashbord for ${this.dtp.config.site.name}`;
res.render('admin/index');
}

@ -33,8 +33,6 @@ class CoreNodeController extends SiteController {
router.get('/:coreNodeId', this.getCoreNodeView.bind(this));
router.get('/', this.getIndex.bind(this));
router.delete('/:coreNodeId', this.deleteCoreNode.bind(this));
return router;
}
@ -139,23 +137,6 @@ class CoreNodeController extends SiteController {
return next(error);
}
}
async deleteCoreNode (req, res) {
const { coreNode: coreNodeService } = this.dtp.services;
try {
await coreNodeService.disconnect(res.locals.coreNode);
const displayList = this.createDisplayList('core-disconnect');
displayList.navigateTo('/admin/core-node');
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to disconnect from Core', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {

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

@ -89,6 +89,10 @@ class PostController extends SiteController {
}
async getComposer (req, res) {
const { post: postService } = this.dtp.services;
if (!res.locals.post) {
res.locals.post = await postService.createPlaceholder(req.user);
}
res.render('post/editor');
}

@ -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,15 @@ class SettingsController extends SiteController {
});
router.post('/', this.postUpdateSettings.bind(this));
router.post('/images/updateSiteIcon', imageUpload.single('imageFile'), this.postUpdateSiteIcon.bind(this));
router.post('/images/updatePostImage', imageUpload.single('imageFile'), this.postUpdatePostImage.bind(this));
router.get('/', this.getSettingsView.bind(this));
router.get('/images', this.getImageSettings.bind(this));
return router;
}
@ -47,6 +60,68 @@ 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.locals.postImage = await imageService.getPostImageInfo();
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,
});
}
}
async postUpdatePostImage (req, res) {
const { image: imageService } = this.dtp.services;
try {
const displayList = this.createDisplayList('site-post-image');
await imageService.updatePostImage(req.body, req.file);
displayList.showNotification(
'Post Image 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 = {

@ -50,9 +50,14 @@ class AuthorController extends SiteController {
router.use(checkPermissions);
router.get('/post',
router.get('/posts',
limiterService.createMiddleware(authorLimiter.getPostIndex),
this.getPostHome.bind(this),
this.getPublishedPostHome.bind(this),
);
router.get('/drafts',
limiterService.createMiddleware(authorLimiter.getPostIndex),
this.getDraftsHome.bind(this),
);
router.get('/',
@ -61,15 +66,34 @@ class AuthorController extends SiteController {
);
}
async getPostHome (req, res, next) {
async getPublishedPostHome (req, res, next) {
const { post: postService } = this.dtp.services;
try {
res.locals.drafts = await postService.getForAuthor(req.user, ['draft'], { skip: 0, cpp: 5 });
res.locals.archive = await postService.getForAuthor(req.user, ['archived'], { skip: 0, cpp: 5 });
const isAdmin = req.user.flags.isAdmin;
const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts;
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.published = await postService.getForAuthor(req.user, ['published'], res.locals.pagination);
this.log.debug('published posts for author', { count: res.locals.published.totalPostCount });
if(canAuthor) {
if ( canPublish ) {
res.locals.published = await postService.getPosts( { skip: 0, cpp: 5 }, ['published'], true );
res.locals.allPosts = true;
res.locals.published.all = true;
} else {
res.locals.published = await postService.getForAuthor( req.user, ['published'], { skip: 0, cpp: 5 } );
}
}
else if ( canPublish || isAdmin ) {
res.locals.published = await postService.getPosts( { skip: 0, cpp: 5 }, ['published'], true );
res.locals.published.all = true;
}
res.render('author/post/index');
} catch (error) {
@ -78,6 +102,43 @@ class AuthorController extends SiteController {
}
}
async getDraftsHome (req, res, next) {
const { post: postService } = this.dtp.services;
try {
const isAdmin = req.user.flags.isAdmin;
const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts;
res.locals.pagination = this.getPaginationParameters(req, 20);
const status = ['draft'];
if(canAuthor) {
if ( canPublish ) {
res.locals.drafts = await postService.getPosts( { skip: 0, cpp: 5 }, status, true );
res.locals.allPosts = true;
res.locals.drafts.all = true;
} else {
res.locals.drafts = await postService.getForAuthor( req.user, status, { skip: 0, cpp: 5 } );
}
}
else if ( canPublish || isAdmin ) {
res.locals.drafts = await postService.getPosts( { skip: 0, cpp: 5 }, status, true );
res.locals.drafts.all = true;
}
res.render('author/draft/index');
} catch (error) {
this.log.error('failed to render Author dashboard', { error });
return next(error);
}
}
async getAuthorHome (req, res, next) {
const { /*comment: commentService,*/ post: postService } = this.dtp.services;
try {
@ -86,9 +147,9 @@ class AuthorController extends SiteController {
const canAuthor = req.user.permissions.canAuthorPosts;
const canPublish = req.user.permissions.canPublishPosts;
if(canAuthor) {
if(canAuthor || isAdmin) {
if(canPublish) {
if(canPublish || isAdmin) {
res.locals.published = await postService.getPosts({ skip: 0, cpp: 5 });
res.locals.drafts = await postService.getPosts({ skip: 0, cpp: 5 }, ['draft']);
@ -105,7 +166,7 @@ class AuthorController extends SiteController {
res.locals.authorComments = await postService.getCommentsForAuthor(req.user, res.locals.pagination);
}
}
else if (canPublish || isAdmin) {
else if (canPublish) {
res.locals.posts = await postService.getPosts({ skip: 0, cpp: 5 }, ['draft', 'published', 'archived']);
res.locals.posts.all = true;
}

@ -28,8 +28,6 @@ class NewsroomController extends SiteController {
router.param('feedId', this.populateFeedId.bind(this));
router.get('/feed', this.getUnifiedFeed.bind(this));
router.get('/:feedId',
limiterService.createMiddleware(limiterService.config.newsroom.getFeedView),
this.getFeedView.bind(this),
@ -55,30 +53,6 @@ class NewsroomController extends SiteController {
}
}
async getUnifiedFeed (req, res) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.newsroom = await feedService.getNewsfeed(res.locals.pagination);
switch (req.query.fmt) {
case 'json':
res.status(200).json(res.locals.newsroom);
break;
default:
res.render('newsroom/unified-feed');
break;
}
} catch (error) {
this.log.error('failed to present newsfeed JSON', { error });
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async getFeedView (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {

@ -52,7 +52,7 @@ class PageController extends SiteController {
const { resource: resourceService } = this.dtp.services;
try {
if (res.locals.page.status === 'published') {
await resourceService.recordView(req, 'Page', res.locals.page._id);
await resourceService.recordView(req, 'Page', res.locals.page._id, res);
}
res.locals.pageSlug = res.locals.page.slug;
res.locals.pageTitle = `${res.locals.page.title} on ${this.dtp.config.site.name}`;

@ -30,8 +30,11 @@ class PostController extends SiteController {
dtp.app.use('/post', router);
async function requireAuthorPrivileges (req, res, next) {
if (!req.user || !req.user.permissions.canAuthorPages) {
return next(new SiteError(403, 'Author privileges are required'));
if (req.user && req.user.flags.isAdmin) {
return next();
}
if (!req.user || !req.flags.isAdmin) {
return next(new SiteError(403, 'Author or admin privileges are required'));
}
return next();
}
@ -41,6 +44,7 @@ class PostController extends SiteController {
return next();
});
router.param('username', this.populateUsername.bind(this));
router.param('postSlug', this.populatePostSlug.bind(this));
router.param('postId', this.populatePostId.bind(this));
@ -52,6 +56,8 @@ class PostController extends SiteController {
router.post('/:postId/image', requireAuthorPrivileges, upload.single('imageFile'), this.postUpdateImage.bind(this));
router.post('/:postId', requireAuthorPrivileges, this.postUpdatePost.bind(this));
router.post('/:postId/tags', requireAuthorPrivileges, this.postUpdatePostTags.bind(this));
router.post('/', requireAuthorPrivileges, this.postCreatePost.bind(this));
router.get('/:postId/edit', requireAuthorPrivileges, this.getEditor.bind(this));
@ -62,6 +68,16 @@ class PostController extends SiteController {
this.getComments.bind(this),
);
router.get('/author/:username',
limiterService.createMiddleware(limiterService.config.post.getIndex),
this.getAuthorView.bind(this),
);
router.get('/authors',
limiterService.createMiddleware(limiterService.config.post.getAllAuthorsView),
this.getAllAuthorsView.bind(this),
);
router.get('/:postSlug',
limiterService.createMiddleware(limiterService.config.post.getView),
this.getView.bind(this),
@ -84,6 +100,22 @@ class PostController extends SiteController {
requireAuthorPrivileges,
this.deletePost.bind(this),
);
router.get('/*', this.badRoute.bind(this) );
}
async populateUsername (req, res, next, username) {
const { user: userService } = this.dtp.services;
try {
res.locals.author = await userService.lookup(username);
if (!res.locals.author) {
throw new SiteError(404, 'User not found');
}
return next();
} catch (error) {
this.log.error('failed to populate username', { username, error });
return next(error);
}
}
async populatePostSlug (req, res, next, postSlug) {
@ -201,17 +233,46 @@ class PostController extends SiteController {
async postUpdatePost (req, res, next) {
const { post: postService } = this.dtp.services;
try {
if (!req.user._id.equals(res.locals.post.author._id) &&
!req.user.permissions.canPublishPosts) {
throw new SiteError(403, 'This is not your post');
if(!req.user.flags.isAdmin){
if (!req.user._id.equals(res.locals.post.author._id) ||
!req.user.permissions.canPublishPosts) {
throw new SiteError(403, 'This is not your post');
}
}
await postService.update(req.user, res.locals.post, req.body);
res.redirect(`/post/${res.locals.post.slug}`);
} catch (error) {
this.log.error('failed to update post', { newletterId: res.locals.post._id, error });
this.log.error('failed to update post', { postId: res.locals.post._id, error });
return next(error);
}
}
async postUpdatePostTags (req, res) {
const { post: postService } = this.dtp.services;
try {
if(!req.user.flags.isAdmin)
{
if (!req.user._id.equals(res.locals.post.author._id)) {
throw new SiteError(403, 'Only authors or admins can update tags.');
}
}
await postService.updateTags(req.user, res.locals.post, req.body);
const displayList = this.createDisplayList();
displayList.showNotification(
'Profile photo updated successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to update post tags', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postCreatePost (req, res, next) {
const { post: postService } = this.dtp.services;
@ -278,7 +339,7 @@ class PostController extends SiteController {
}
}
if (res.locals.post.status === 'published') {
await resourceService.recordView(req, 'Post', res.locals.post._id);
await resourceService.recordView(req, 'Post', res.locals.post._id, res);
}
res.locals.countPerPage = 20;
@ -294,6 +355,10 @@ class PostController extends SiteController {
res.locals.pagination,
);
res.locals.pageTitle = `${res.locals.post.title} on ${this.dtp.config.site.name}`;
res.locals.pageDescription = `${res.locals.post.summary}`;
if (res.locals.post.image) {
res.locals.shareImage = `https://${this.dtp.config.site.domain}/image/${res.locals.post.image._id}`;
}
res.render('post/view');
} catch (error) {
this.log.error('failed to service post view', { postId: res.locals.post._id, error });
@ -327,18 +392,47 @@ class PostController extends SiteController {
}
}
async getAuthorView (req, res, next) {
const { post: postService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
const {posts, totalPostCount} = await postService.getForAuthor(res.locals.author, ['published'], res.locals.pagination);
res.locals.posts = posts;
res.locals.totalPostCount = totalPostCount;
res.render('post/author/view');
} catch (error) {
return next(error);
}
}
async getAllAuthorsView (req, res, next) {
const { user: userService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
const {authors , totalAuthorCount }= await userService.getAuthors(res.locals.pagination);
res.locals.authors = authors;
res.locals.totalAuthorCount = totalAuthorCount;
res.render('post/author/all');
} catch (error) {
return next(error);
}
}
async deletePost (req, res) {
const { post: postService } = this.dtp.services;
try {
if (!req.user._id.equals(res.locals.post.author._id) ||
!req.user.permissions.canPublishPosts) {
throw new SiteError(403, 'This is not your post');
// only give admins and the author permission to delete
if (!req.user.flags.isAdmin) {
if (!req.user._id.equals(res.locals.post.author._id)) {
throw new SiteError(403, 'This is not your post');
}
}
await postService.deletePost(res.locals.post);
const displayList = this.createDisplayList('add-recipient');
displayList.reload();
displayList.navigateTo('/');
res.status(200).json({ success: true, displayList });
} catch (error) {

@ -0,0 +1,97 @@
// post.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 TagController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const { dtp } = this;
// const {
// post: postService,
// limiter: limiterService,
// session: sessionService,
// } = dtp.services;
// const authRequired = sessionService.authCheckMiddleware({ requiredLogin: true });
const router = express.Router();
dtp.app.use('/tag', router);
router.use(async (req, res, next) => {
res.locals.currentView = 'home';
return next();
});
router.param('tagSlug', this.populateTagSlug.bind(this));
router.get('/:tagSlug', this.getSearchView.bind(this));
}
async populateTagSlug (req, res, next, tagSlug) {
const { post: postService } = this.dtp.services;
try {
var allPosts = false;
var statusArray = ['published'];
if (req.user) {
if (req.user.flags.isAdmin) {
statusArray.push('draft');
allPosts = true;
}
}
res.locals.allPosts = allPosts;
res.locals.tagSlug = tagSlug;
tagSlug = tagSlug.replace("_", " ");
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getByTags(tagSlug, res.locals.pagination, statusArray);
res.locals.tag = tagSlug;
return next();
} catch (error) {
this.log.error('failed to populate tagSlug', { tagSlug, error });
return next(error);
}
}
async getSearchView (req, res) {
try {
res.locals.pageTitle = `Tag ${res.locals.tag} on ${this.dtp.config.site.name}`;
res.render('tag/view');
} catch (error) {
this.log.error('failed to service post view', { postId: res.locals.post._id, error });
throw SiteError("Error getting tag view:", error );
}
}
async getIndex (req, res, next) {
const { post: postService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination);
res.render('tag/index');
} catch (error) {
return next(error);
}
}
}
module.exports = {
slug: 'tag',
name: 'tag',
create: async (dtp) => {
let controller = new TagController(dtp);
return controller;
},
};

@ -20,7 +20,6 @@ const KaleidoscopeEventSchema = new Schema({
href: { type: String },
thumbnail: { type: String },
source: {
client: { type: Schema.ObjectId, index: 1, ref: 'OAuth2Client' },
pkg: {
name: { type: String, required: true },
version: { type: String, required: true },

@ -9,17 +9,11 @@ const Schema = mongoose.Schema;
module.exports.DTP_THEME_LIST = ['dtp-light', 'dtp-dark'];
module.exports.DTP_USER_TYPE_LIST = ['CoreUser', 'User'];
module.exports.DtpUserSchema = new Schema({
userType: { type: String, enum: module.exports.DTP_USER_TYPE_LIST, required: true },
user: { type: Schema.ObjectId, required: true, index: true, refPath: 'userType' },
}, { _id: false });
module.exports.UserFlagsSchema = new Schema({
isAdmin: { type: Boolean, default: false, required: true },
isModerator: { type: Boolean, default: false, required: true },
isEmailVerified: { type: Boolean, default: false, required: true },
}, { _id: false });
});
module.exports.UserPermissionsSchema = new Schema({
canLogin: { type: Boolean, default: true, required: true },
@ -30,9 +24,9 @@ module.exports.UserPermissionsSchema = new Schema({
canAuthorPosts: { type: Boolean, default: false, required: true },
canPublishPages: { type: Boolean, default: false, required: true },
canPublishPosts: { type: Boolean, default: false, required: true },
}, { _id: false });
});
module.exports.UserOptInSchema = new Schema({
system: { type: Boolean, default: true, required: true },
marketing: { type: Boolean, default: true, required: true },
}, { _id: false });
});

@ -1,53 +0,0 @@
// media-router.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const STATUS_LIST = [
'starting', // the router process is starting and configuring itself
'active', // the router is active and available for service
'capacity', // the router is at or over capacity
'closing', // the router is closing/shutting down
'closed', // the router no longer exists
];
const RouterHostSchema = new Schema({
address: { type: String, required: true, index: 1 },
port: { type: Number, required: true },
});
/*
* A Router is a "multi-user conference call instance" somewhere on the
* infrastructure. This model helps us manage them, balance load across them,
* and route calls to and between them (for scale).
*
* These records are created when a call is being created, and are commonly
* left in the database after all call participants have left. An expires index
* is used to sweep up router records after 30 days. This allows us to perform
* statistics aggregation on router use and store aggregated results as part of
* long-term reporting.
*/
const MediaRouterSchema = new Schema({
created: { type: Date, default: Date.now, required: true, expires: '30d' },
lastActivity: { type: Date, default: Date.now, required: true },
status: { type: String, enum: STATUS_LIST, default: 'starting', required: true, index: true },
name: { type: String },
description: { type: String },
access: {
isPrivate: { type: Boolean, default: true, required: true },
passcodeHash: { type: String, select: false },
},
host: { type: RouterHostSchema, required: true, select: false },
stats: {
routerCount: { type: Number, default: 0, required: true },
consumerCount: { type: Number, default: 0, required: true },
producerCount: { type: Number, default: 0, required: true },
}
});
module.exports = mongoose.model('MediaRouter', MediaRouterSchema);

@ -1,43 +0,0 @@
// media-worker.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const STATUS_LIST = [
'starting', // the router process is starting and configuring itself
'active', // the router is active and available for service
'capacity', // the router is at or over capacity
'closing', // the router is closing/shutting down
'closed', // the router no longer exists
];
const WebRtcListenSchema = new Schema({
protocol: { type: String, enum: ['tcp','udp'], required: true },
ip: { type: String, required: true },
port: { type: Number, required: true },
});
/*
* A media worker is a host process with one or more MediaRouter instances
* processing multi-user conference calls.
*/
const MediaWorkerSchema = new Schema({
created: { type: Date, default: Date.now, required: true, expires: '30d' },
lastActivity: { type: Date, default: Date.now, required: true },
status: { type: String, enum: STATUS_LIST, default: 'starting', required: true, index: true },
webRtcServer: {
listenInfos: { type: [WebRtcListenSchema] },
},
stats: {
routerCount: { type: Number, default: 0, required: true },
consumerCount: { type: Number, default: 0, required: true },
producerCount: { type: Number, default: 0, required: true },
}
});
module.exports = mongoose.model('MediaWorker', MediaWorkerSchema);

@ -23,6 +23,7 @@ const PostSchema = new Schema({
author: { type: Schema.ObjectId, required: true, index: 1, refPath: 'authorType' },
image: { type: Schema.ObjectId, ref: 'Image' },
title: { type: String, required: true },
tags: { type: [String], lowercase: true },
slug: { type: String, required: true, lowercase: true, unique: true },
summary: { type: String },
content: { type: String, select: false },

@ -7,11 +7,9 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const { DtpUserSchema } = require('./lib/user-types.js');
const UserBlockSchema = new Schema({
member: { type: DtpUserSchema, required: true },
blockedMembers: { type: [DtpUserSchema] },
user: { type: Schema.ObjectId, required: true, index: true, ref: 'User' },
blockedUsers: { type: [Schema.ObjectId], ref: 'User' },
});
module.exports = mongoose.model('UserBlock', UserBlockSchema);

@ -45,21 +45,17 @@ const UserSchema = new Schema({
});
UserSchema.virtual('hasAuthorPermissions').get( function ( ) {
return !!this && !!this.permissions && (this.permissions.canAuthorPages || this.permissions.canAuthorPosts);
return this.permissions.canAuthorPosts;
});
UserSchema.virtual('hasPublishPermissions').get( function ( ) {
return !!this && !!this.permissions && (this.permissions.canPublishPages || this.permissions.canPublishPosts);
return this.permissions.canPublishPages || this.permissions.canPublishPosts;
});
UserSchema.virtual('hasAuthorDashboard').get( function ( ) {
if (!this || !this.permissions) {
return false;
}
return this.permissions.canAuthorPages ||
this.permissions.cahAuthorPosts ||
this.permissions.canPublishPages ||
this.permissions.canPublishPosts;
return this.permissions.cahAuthorPosts ||
this.permissions.canPublishPosts ||
this.flags.isAdmin;
});
module.exports = mongoose.model('User', UserSchema);

@ -61,12 +61,6 @@ class CoreNodeService extends SiteService {
async start ( ) {
await super.start();
const https = require('https');
this.httpsAgent = new https.Agent({
// read it out-loud: Reject unauthorized when not the 'local' environment.
rejectUnauthorized: (process.env.NODE_ENV !== 'local'),
});
const cores = await this.getConnectedCores(null, true);
this.log.info('Core Node service starting', { connectedCoreCount: cores.length });
@ -193,7 +187,7 @@ class CoreNodeService extends SiteService {
url: '/core/info/package',
});
core = await CoreNode.findOneAndUpdate(
await CoreNode.updateOne(
{ _id: core._id },
{
$set: {
@ -207,11 +201,10 @@ class CoreNodeService extends SiteService {
'meta.supportEmail': txSite.response.site.supportEmail,
},
},
{ new: true },
);
core = await CoreNode.findOne({ _id: core._id }).lean();
this.log.info('resolved Core node', { core });
this.emitDtpEvent('resolve', { core, host });
return { core, networkPolicy: txSite.response.site.networkPolicy };
}
@ -264,8 +257,6 @@ class CoreNodeService extends SiteService {
body: { event },
};
await this.emitDtpEvent('kaleidoscope.event', { event, recipients, request });
if (!recipients) {
return this.broadcast(request);
}
@ -312,7 +303,6 @@ class CoreNodeService extends SiteService {
async broadcast (request) {
const results = [ ];
await this.emitDtpEvent('kaleidoscope.broadcast', { request });
await CoreNode
.find({
'flags.isConnected': true,
@ -337,7 +327,6 @@ class CoreNodeService extends SiteService {
try {
const req = new CoreNodeRequest();
const options = {
agent: this.httpsAgent,
headers: {
'Content-Type': 'application/json',
},
@ -364,10 +353,8 @@ class CoreNodeService extends SiteService {
options.body = JSON.stringify(request.body);
}
const requestUrl = this.getCoreRequestUrl(core, request.url);
await this.emitDtpEvent('kaleidoscope.request', { core, request, requestUrl });
this.log.info('sending Core node request', { request: req });
const requestUrl = this.getCoreRequestUrl(core, request.url);
const response = await fetch(requestUrl, options);
if (!response.ok) {
let json;
@ -397,10 +384,6 @@ class CoreNodeService extends SiteService {
async setRequestResponse (request, response, json) {
const DONE = new Date();
const ELAPSED = DONE.valueOf() - request.created.valueOf();
/*
* Build the default update operation
*/
const updateOp = {
$set: {
'response.received': DONE,
@ -408,20 +391,9 @@ class CoreNodeService extends SiteService {
'response.statusCode': response.status,
},
};
if (json) {
updateOp.$set['response.success'] = json.success;
}
/*
* Provide an opportunity for anything to alter the operation or cancel it.
*/
await this.emitDtpEvent('kaleidoscope.response', {
core: request.core,
request, response, json,
updateOp,
});
await CoreNodeRequest.updateOne({ _id: request._id }, updateOp);
}
@ -461,62 +433,6 @@ class CoreNodeService extends SiteService {
},
},
);
await this.emitDtpEvent('connect', { core: request.core, request });
}
async disconnect (core) {
this.log.alert('disconnecting from Core', {
name: core.meta.name,
domain: core.meta.domain,
});
// provides an abort point if any listener throws
await this.emitDtpEvent('disconnect-pre', { core });
let disconnect;
try {
disconnect = await this.sendRequest(core, {
method: 'DELETE',
url: `/core/connect/node/${core.oauth.clientId}`,
});
} catch (error) {
if ((error.code !== 'EPROTO') && (error.statusCode !== 404)) {
throw new SiteError(error.statusCode, 'Failed to disconnect from Core');
}
} finally {
this.log.alert('Core disconnect request complete', {
name: core.meta.name,
domain: core.meta.domain,
disconnect,
});
}
try {
await this.emitDtpEvent('disconnect-post', { core, disconnect });
} catch (error) {
this.log.error('failed to emit dtp.core.disconnect-post', { error });
// keep going
}
await CoreUser
.find({ core: core._id })
.cursor()
.eachAsync(this.removeUser.bind(this, core), 1);
await CoreNodeConnect.deleteMany({ 'site.domainKey': core.meta.domainKey });
await CoreNodeRequest.deleteMany({ core: core._id });
try {
await this.emitDtpEvent('disconnect', { core, disconnect });
} catch (error) {
this.log.error('failed to emit dtp.core.disconnect', { error });
// keep going
}
await CoreNode.deleteOne({ _id: core._id });
return disconnect;
}
async queueServiceNodeConnect (requestToken, appNode) {
@ -562,10 +478,7 @@ class CoreNodeService extends SiteService {
await request.save();
request = request.toObject();
await this.emitDtpEvent('service-node.connect', { request });
return request;
return request.toObject();
}
async getServiceNodeQueue (pagination) {
@ -585,6 +498,7 @@ class CoreNodeService extends SiteService {
return request;
}
async acceptServiceNode (requestToken, appNode) {
const { oauth2: oauth2Service } = this.dtp.services;
const response = { token: requestToken };
@ -592,15 +506,13 @@ class CoreNodeService extends SiteService {
this.log.info('accepting app node', { requestToken, appNode });
response.client = await oauth2Service.createClient(appNode.site);
await this.emitDtpEvent('service-node.accept', { client: response.client });
return response;
}
async setCoreOAuth2Credentials (core, credentials) {
const { client } = credentials;
this.log.info('updating Core Connect credentials', { core, client });
core = await CoreNode.findOneAndUpdate(
await CoreNode.updateOne(
{ _id: core._id },
{
$set: {
@ -612,9 +524,7 @@ class CoreNodeService extends SiteService {
'kaleidoscope.token': client.kaleidoscope.token,
},
},
{ new: true },
);
await this.emitDtpEvent('set-oauth2-credentials', { core });
}
registerPassportCoreOAuth2 (core) {
@ -681,7 +591,6 @@ class CoreNodeService extends SiteService {
);
user = user.toObject();
user.type = 'CoreUser';
this.emitDtpEvent('user.login', { user });
return cb(null, user);
} catch (error) {
return cb(error);
@ -804,11 +713,6 @@ class CoreNodeService extends SiteService {
},
);
}
async removeUser (core, user) {
this.log.alert('remove Core user', { core: core.meta.name, user: user.username });
await this.emitDtpEvent('user.remove', { core, user });
}
}
module.exports = {

@ -7,8 +7,6 @@
const mongoose = require('mongoose');
const UserSubscription = mongoose.model('UserSubscription');
const UserNotification = mongoose.model('UserSubscription');
const KaleidoscopeEvent = mongoose.model('KaleidoscopeEvent');
const slug = require('slug');
@ -22,25 +20,6 @@ class HiveService extends SiteService {
super(dtp, module.exports);
}
async start ( ) {
const { oauth2: oauth2Service } = this.dtp.services;
this.eventHandlers = {
onOAuth2RemoveClient: this.onOAuth2RemoveClient.bind(this),
};
oauth2Service.on(oauth2Service.getEventName('client.remove'), this.eventHandlers.onOAuth2RemoveClient);
}
async stop ( ) {
const { oauth2: oauth2Service } = this.dtp.services;
oauth2Service.off(oauth2Service.getEventName('client.remove'), this.eventHandlers.onOAuth2RemoveClient);
delete this.eventHandlers.onOAuth2RemoveClient;
delete this.eventHandlers;
}
async subscribe (user, client, emitterId) {
await UserSubscription.updateOne(
{ user: user._id },
@ -106,7 +85,7 @@ class HiveService extends SiteService {
throw new SiteError(403, 'Unknown client domain key');
}
const event = await this.createKaleidoscopeEvent(eventDefinition, client);
const event = await this.createKaleidoscopeEvent(eventDefinition);
await UserSubscription
.find({
'subscriptions.client': client._id,
@ -121,7 +100,7 @@ class HiveService extends SiteService {
this.emit('kaleidoscope:event', event, client);
}
async createKaleidoscopeEvent (eventDefinition, sourceClient) {
async createKaleidoscopeEvent (eventDefinition) {
const NOW = new Date();
/*
@ -206,16 +185,8 @@ class HiveService extends SiteService {
throw new SiteError(406, 'Missing source emitter href');
}
/*
* Create the KaleidoscopeEvent document
*/
const event = new KaleidoscopeEvent();
if (eventDefinition.created) {
event.created = new Date(eventDefinition.created);
} else {
event.created = NOW;
}
event.created = NOW;
if (eventDefinition.recipientType && eventDefinition.recipient) {
event.recipientType = eventDefinition.recipientType;
@ -248,10 +219,6 @@ class HiveService extends SiteService {
},
};
if (sourceClient) {
event.source.client = sourceClient._id;
}
if (eventDefinition.source.emitter) {
event.source.emitter = {
emitterType: striptags(eventDefinition.source.emitter.emitterType),
@ -306,27 +273,6 @@ class HiveService extends SiteService {
.lean();
return { events, totalEventCount };
}
/*
* OAuth2 event handlers
*/
/**
* This event fires when an OAuth2Client is being disconnected and removed by a
* Core, or a client app is being removed from a Service Node. The Hive service
* will remove all KaleidoscopeEvent records created on behalf of the client.
* @param {OAuth2Client} client the client being removed
*/
async onOAuth2RemoveClient (client) {
this.log.alert('removing KaleidoscopeEvent records from OAuth2Client', { clientId: client._id, domain: client.site.domain });
await KaleidoscopeEvent
.find({ 'source.client': client._id })
.cursor()
.eachAsync(async (event) => {
await UserNotification.deleteMany({ event: event._id });
await KaleidoscopeEvent.deleteOne({ _id: event._id });
}, 1);
}
}
module.exports = {

@ -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,124 @@ 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 getPostImageInfo() {
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
let icon;
try {
await fs.promises.access(siteImagesDir);
const iconMetadata = await sharp(path.join(siteImagesDir, 'default-poster.jpg')).metadata();
icon = {
metadata: iconMetadata,
path: `/img/default-poster.jpg`,
};
} catch (error) {
icon = null;
}
return icon;
}
async updatePostImage(imageDefinition, file) {
this.log.debug('updating site icon', { imageDefinition, file });
try {
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
const sourceIconFilePath = file.path;
await sharp(sourceIconFilePath).resize({
fit: sharp.fit.inside,
width: 540,
height: 960,
}).jpeg()
.toFile(path.join(siteImagesDir, `default-poster.jpg`));
return path.join(siteImagesDir, 'default-poster.jpg');
} 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);
}
}
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.inside,
width: size,
height: size,
}).png()
.toFile(path.join(siteIconDir, `icon-${size}x${size}.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,

@ -464,10 +464,6 @@ class OAuth2Service extends SiteService {
* @param {OAuth2Client} client the client to be removed
*/
async removeClient (client) {
// provides opportunity to allow or disallow and, if allowed, perform any
// additional cleanup needed when removing a client.
await this.emitDtpEvent('client.remove', client);
this.log.info('removing client', { clientId: client._id, });
await OAuth2Client.deleteOne({ _id: client._id });
}

@ -10,7 +10,7 @@ const mongoose = require('mongoose');
const OtpAccount = mongoose.model('OtpAccount');
const ONE_HOUR = 1000 * 60 * 60;
const OTP_SESSION_DURATION = (process.env.NODE_ENV === 'local') ? (ONE_HOUR * 24) : (ONE_HOUR * 2);
const OTP_SESSION_DURATION = (process.env.NODE_ENV === 'local') ? (ONE_HOUR * 24) : (ONE_HOUR * 8);
const { authenticator } = require('otplib');
const uuidv4 = require('uuid').v4;
@ -209,6 +209,11 @@ class OtpAuthService extends SiteService {
return true;
}
async destroyOtpSession (req, serviceName) {
delete req.session.otp[serviceName];
await this.saveSession(req);
}
async isUserProtected (user, serviceName) {
const account = await OtpAccount.findOne({ user: user._id, service: serviceName });
if (!account) {
@ -217,8 +222,15 @@ class OtpAuthService extends SiteService {
return true;
}
async removeForUser (user) {
return await OtpAccount.deleteMany({ user: user });
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;
}
}

@ -49,6 +49,27 @@ class PageService extends SiteService {
}
}
async createPlaceholder (author) {
const NOW = new Date();
if (!author.flags.isAdmin) {
throw new SiteError(403, 'You are not permitted to author pages');
}
let page = new Page();
page.created = NOW;
page.authorType = author.type;
page.author = author._id;
page.title = "New Draft page";
page.slug = `draft-page-${page._id}`;
await page.save();
page = page.toObject();
page.author = author; // self-populate instead of calling db
return page;
}
async create (author, pageDefinition) {
if (!author.permissions.canAuthorPages) {
throw new SiteError(403, 'You are not permitted to author pages');
@ -82,9 +103,9 @@ class PageService extends SiteService {
},
};
if (!user.permissions.canAuthorPages) {
throw new SiteError(403, 'You are not permitted to author or change pages.');
}
// if (!user.permissions.canAuthorPages) {
// throw new SiteError(403, 'You are not permitted to author or change pages.');
// }
if (pageDefinition.title) {
updateOp.$set.title = striptags(pageDefinition.title.trim());

@ -42,8 +42,10 @@ class PostService extends SiteService {
async createPlaceholder (author) {
const NOW = new Date();
if (!author.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to author posts');
if (!author.flags.isAdmin){
if (!author.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to author posts');
}
}
let post = new Post();
@ -63,11 +65,19 @@ class PostService extends SiteService {
async create (author, postDefinition) {
const NOW = new Date();
if (!author.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to author posts');
if (!author.flags.isAdmin){
if (!author.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to author posts');
}
if ((postDefinition.status === 'published') && !author.permissions.canPublishPosts) {
throw new SiteError(403, 'You are not permitted to publish posts');
}
}
if ((postDefinition.status === 'published') && !author.permissions.canPublishPosts) {
throw new SiteError(403, 'You are not permitted to publish posts');
if (postDefinition.tags) {
postDefinition.tags = postDefinition.tags.split(',').map((tag) => striptags(tag.trim()));
} else {
postDefinition.tags = [ ];
}
const post = new Post();
@ -78,6 +88,7 @@ class PostService extends SiteService {
post.slug = this.createPostSlug(post._id, post.title);
post.summary = striptags(postDefinition.summary.trim());
post.content = postDefinition.content.trim();
post.tags = postDefinition.tags;
post.status = postDefinition.status || 'draft';
post.flags = {
enableComments: postDefinition.enableComments === 'on',
@ -92,10 +103,11 @@ class PostService extends SiteService {
async update (user, post, postDefinition) {
const { coreNode: coreNodeService } = this.dtp.services;
if (!user.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to author posts');
if (!user.flags.isAdmin){
if (!user.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to author posts');
}
}
const NOW = new Date();
const updateOp = {
$setOnInsert: {
@ -125,12 +137,18 @@ class PostService extends SiteService {
if (postDefinition.content) {
updateOp.$set.content = postDefinition.content.trim();
}
await this.updateTags(post._id, postDefinition.tags);
if (!postDefinition.status) {
throw new SiteError(406, 'Must include post status');
}
if (post.status !== 'published' && postDefinition.status === 'published') {
if (!user.permissions.canPublishPosts) {
// const postWillBeUnpublished = post.status === 'published' && postDefinition.status !== 'published';
const postWillBePublished = post.status !== 'published' && postDefinition.status === 'published';
if (postWillBePublished) {
if (!user.permissions.canPublishPosts && !user.flags.isAdmin) {
throw new SiteError(403, 'You are not permitted to publish posts');
}
}
@ -166,6 +184,42 @@ class PostService extends SiteService {
}
}
// pass the post._id and its tags to function
async updateTags (id, tags) {
if (tags) {
tags = tags.split(',').map((tag) => striptags(tag.trim().toLowerCase()));
} else {
tags = [ ];
}
const NOW = new Date();
const updateOp = {
$setOnInsert: {
created: NOW,
},
$set: {
updated: NOW,
},
};
updateOp.$set.tags = tags;
await Post.findOneAndUpdate(
{ _id: id },
updateOp,
);
}
async getByTags (tag, pagination, status = ['published']) {
if (!Array.isArray(status)) {
status = [status];
}
const posts = await Post.find( { status: { $in: status }, tags: tag } )
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populatePost);
return posts;
}
async updateImage (user, post, file) {
const { image: imageService } = this.dtp.services;
@ -194,12 +248,22 @@ class PostService extends SiteService {
);
}
async getPosts (pagination, status = ['published']) {
async getPosts (pagination, status = ['published'], count = false) {
if (!Array.isArray(status)) {
status = [status];
}
var search = {
status: { $in: status },
'flags.isFeatured': false
};
if ( count ) {
search = {
status: { $in: status },
};
}
const posts = await Post
.find({ status: { $in: status }, 'flags.isFeatured': false })
.find(search)
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
@ -208,6 +272,11 @@ class PostService extends SiteService {
posts.forEach((post) => {
post.author.type = post.authorType;
});
if (count) {
const totalPostCount = await Post
.countDocuments(search);
return { posts, totalPostCount };
}
return posts;
}

@ -34,7 +34,7 @@ class ResourceService extends SiteService {
* @param {mongoose.Types.ObjectId} resourceId The _id of the object for which
* a view is being tracked.
*/
async recordView (req, resourceType, resourceId) {
async recordView (req, resourceType, resourceId, res) {
const Model = mongoose.model(resourceType);
const modelUpdate = { $inc: { } };
@ -44,6 +44,16 @@ class ResourceService extends SiteService {
let uniqueKey = req.ip.toString().trim().toLowerCase();
if (req.user) {
if (resourceType === 'Post') {
if (req.user._id.equals(res.locals.post.author._id)) {
return;
}
}
if (resourceType === 'Page') {
if (req.user._id.equals(res.locals.page.author._id)) {
return;
}
}
uniqueKey += `:user:${req.user._id.toString()}`;
}

@ -507,7 +507,7 @@ class UserService extends SiteService {
}
decorateUserObject (user) {
user.hasAuthorPermissions = user.permissions.canAuthorPages || user.permissions.canAuthorPosts;
user.hasAuthorPermissions = user.permissions.canAuthorPosts;
user.hasPublishPermissions = user.permissions.canPublishPages || user.permissions.canPublishPosts;
user.hasAuthorDashboard = user.hasAuthorPermissions || user.hasPublishPermissions;
}
@ -528,6 +528,19 @@ class UserService extends SiteService {
return users.map((user) => { user.type = 'User'; return user; });
}
async getAuthors (pagination) {
const authors = await User
.find({ 'permissions.canAuthorPosts': true })
.sort({ displayName: 1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateUser)
.lean();
const totalAuthorCount = await User.countDocuments( {'permissions.canAuthorPosts': true } );
return {authors, totalAuthorCount};
}
async getUserProfile (userId) {
let user;
@ -738,38 +751,33 @@ class UserService extends SiteService {
await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } });
}
async blockUser (user, blockedUser) {
if (user._id.equals(blockedUser._id)) {
async blockUser (userId, blockedUserId) {
userId = mongoose.Types.ObjectId(userId);
blockedUserId = mongoose.Types.ObjectId(blockedUserId);
if (userId.equals(blockedUserId)) {
throw new SiteError(406, "You can't block yourself");
}
await UserBlock.updateOne(
{ 'member.user': user._id },
{ user: userId },
{
$addToSet: {
blockedMembers: {
userType: blockedUser.type,
user: blockedUser._id,
},
},
$addToSet: { blockedUsers: blockedUserId },
},
{ upsert: true },
);
}
async unblockUser (user, blockedUser) {
if (user._id.equals(blockedUser._id)) {
async unblockUser (userId, blockedUserId) {
userId = mongoose.Types.ObjectId(userId);
blockedUserId = mongoose.Types.ObjectId(blockedUserId);
if (userId.equals(blockedUserId)) {
throw new SiteError(406, "You can't un-block yourself");
}
await UserBlock.updateOne(
{ 'member.user': user._id },
{ user: userId },
{
$removeFromSet: {
blockedUsers: {
userType: blockedUser.type,
user: blockedUser._id,
},
},
$removeFromSet: { blockedUsers: blockedUserId },
},
{ upsert: true },
);
}

@ -29,6 +29,7 @@ class VenueService extends SiteService {
async start ( ) {
const { user: userService } = this.dtp.services;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
this.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
@ -46,13 +47,6 @@ class VenueService extends SiteService {
try {
res.locals.venue = res.locals.venue || { };
res.locals.venue.channels = [ ];
if (req.path.startsWith('/image') ||
req.path.startsWith('/auth')) {
return next();
}
this.log.info('populating Venue channel data for route', { path: req.path });
await VenueChannel
.find()
.populate(this.populateVenueChannel)
@ -64,7 +58,7 @@ class VenueService extends SiteService {
});
return next();
} catch (error) {
this.log.error('failed to populate Soapbox channel data for route', { error });
this.log.error('failed to populate Soapbox channel feed', { error });
return next();
}
};

@ -5,7 +5,7 @@ block content
.uk-width-expand
h1 Announcements
.uk-width-auto
a(href="/admin/announcement/create").uk-button.dtp-button-primary.uk-border-rounded
a(href="/admin/announcement/create").uk-button.dtp-button-primary
span
i.fas.fa-plus
span.uk-margin-small-left Create
@ -21,7 +21,7 @@ block content
i(class=`fas ${announcement.title.icon.class}`)
span.uk-margin-small-left= announcement.title.content
.uk-width-auto
button(type="button", data-announcement-id= announcement._id, onclick="return dtp.adminApp.deleteAnnouncement(event);").uk-button.dtp-button-danger.uk-border-rounded
button(type="button", data-announcement-id= announcement._id, onclick="return dtp.adminApp.deleteAnnouncement(event);").uk-button.dtp-button-danger
span
i.fas.fa-trash
else

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

@ -10,8 +10,6 @@ mixin renderCoreNodeListItem (coreNode)
+renderCell('Domain', coreNode.meta.domain)
.uk-width-auto
+renderCell('Domain Key', coreNode.meta.domainKey)
.uk-width-auto
+renderCell('id', coreNode._id)
.uk-margin
div(uk-grid).uk-flex-between

@ -8,33 +8,20 @@ block content
div(uk-grid).uk-grid-small.uk-flex-middle
div(class="uk-width-1-1 uk-width-expand@m")
h1(style="line-height: 1em;") Core Node
.uk-width-auto
div(class="uk-width-1-1 uk-width-auto@m")
a(href=`mailto:${coreNode.meta.supportEmail}?subject=${encodeURIComponent(`Support request from ${site.name}`)}`)
span
i.fas.fa-envelope
span.uk-margin-small-left Email Support
.uk-width-auto
div(class="uk-width-1-1 uk-width-auto@m")
span.uk-label(style="line-height: 1.75em;", class={
'uk-label-success': coreNode.flags.isConnected,
'uk-label-warning': !coreNode.flags.isConnected && !coreNode.flags.isBlocked,
'uk-label-danger': coreNode.flags.isBlocked,
}).no-select= coreNode.flags.isConnected ? 'Connected' : 'Pending'
+renderCoreNodeListItem(coreNode)
.uk-margin
button(
type="button",
data-core={ _id: coreNode._id, name: coreNode.meta.name},
onclick="return dtp.adminApp.disconnectCore(event);",
).uk-button.dtp-button-danger.uk-border-rounded
span
i.fas.fa-window-close
span.uk-margin-small-left Disconnect
.uk-margin
table.uk-table.uk-table-small
thead

@ -21,7 +21,7 @@ block content
data-newsletter-id= newsletter._id,
data-newsletter-title= newsletter.title,
onclick="return dtp.adminApp.deleteNewsletter(event);",
).uk-button.uk-button-danger.uk-border-rounded
).uk-button.uk-button-danger
+renderButtonIcon('fa-trash', 'Delete')
.uk-width-auto
@ -30,7 +30,7 @@ block content
data-newsletter-id= newsletter._id,
data-newsletter-title= newsletter.title,
onclick="return dtp.adminApp.sendNewsletter(event);",
).uk-button.uk-button-default.uk-border-rounded
).uk-button.uk-button-default
+renderButtonIcon('fa-paper-plane', 'Send')
else
div There are no newsletters at this time.

@ -6,59 +6,40 @@ block content
.uk-margin
h1 Job Queue: #{queueName}
div(uk-grid).uk-flex-between
.uk-width-auto
label.uk-form-label.uk-text-primary Active
.uk-text-large= numeral(jobCounts.active).format('0,0')
.uk-width-auto
label.uk-form-label.uk-text-success Completed
.uk-text-large= numeral(jobCounts.completed).format('0,0')
.uk-width-auto
label.uk-form-label.uk-text-warning Delayed
.uk-text-large= numeral(jobCounts.delayed).format('0,0')
.uk-width-auto
label.uk-form-label.uk-text-danger Failed
.uk-text-large= numeral(jobCounts.failed).format('0,0')
.uk-width-auto
label.uk-form-label.uk-text-muted Waiting
.uk-text-large= numeral(jobCounts.waiting).format('0,0')
.uk-width-auto
label.uk-form-label.uk-text-muted Paused
.uk-text-large= numeral(jobCounts.paused).format('0,0')
- var pendingJobCount = jobCounts.waiting + jobCounts.delayed + jobCounts.paused + jobCounts.active
.uk-width-auto Total#[br]#{numeral(pendingJobCount).format('0,0')}
.uk-width-auto Waiting#[br]#{numeral(jobCounts.waiting).format('0,0')}
.uk-width-auto Delayed#[br]#{numeral(jobCounts.delayed).format('0,0')}
.uk-width-auto Paused#[br]#{numeral(jobCounts.paused).format('0,0')}
.uk-width-auto Active#[br]#{numeral(jobCounts.active).format('0,0')}
.uk-width-auto Completed#[br]#{numeral(jobCounts.completed).format('0,0')}
.uk-width-auto Failed#[br]#{numeral(jobCounts.failed).format('0,0')}
div(uk-grid)
div(class="uk-width-1-1 uk-width-1-2@l")
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Active
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.active)
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Active
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.active)
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Delayed
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.delayed)
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Paused
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.paused)
div(class="uk-width-1-1 uk-width-1-2@l")
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Waiting
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.waiting)
div(class="uk-width-1-1 uk-width-1-2@l")
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Waiting
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.waiting)
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Delayed
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.delayed)
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Failed
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.failed)
div(class="uk-width-1-1 uk-width-1-2@l")
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h3.uk-card-title Failed
.uk-card-body
+renderJobQueueJobList(newsletterQueue, jobs.failed)

@ -34,9 +34,6 @@ block content
.uk-card-footer
div(uk-grid).uk-flex-right.uk-flex-middle
.uk-width-expand
+renderBackButton()
if feed
.uk-width-auto
button(
@ -44,13 +41,6 @@ block content
data-feed-id= feed._id,
data-feed-title= feed.title,
onclick="return dtp.adminApp.removeNewsroomFeed(event);",
).uk-button.uk-button-danger.uk-border-rounded
span
i.fas.fa-trash
span.uk-margin-small-left Remove Feed
).uk-button.uk-button-danger.uk-border-rounded Remove Feed
.uk-width-auto
button(type="submit").uk-button.uk-button-primary.uk-border-rounded
span
i.fas.fa-save
span.uk-margin-small-left= feed ? 'Update Feed' : 'Add Feed'
button(type="submit").uk-button.uk-button-primary.uk-border-rounded= feed ? 'Update Feed' : 'Add Feed'

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

@ -23,10 +23,12 @@ block content
.uk-text-small
div(uk-grid).uk-grid-small
.uk-width-auto
span published: #{moment(post.created).format('MMM DD, YYYY [at] hh:mm:ss a')}
span published:
span(data-dtp-timestamp= post.created)
if post.updated
.uk-width-auto
span last update: #{moment(post.updated).format('MMM DD, YYYY [at] hh:mm:ss a')}
span last update:
span(data-dtp-timestamp= post.updated)
.uk-width-auto
span by
a(href=`/admin/user/${post.author._id}`)=` ${post.author.username}`

@ -28,6 +28,16 @@ block content
legend Featured Embed
textarea(id="featured-embed", name="featuredEmbed", rows="4").uk-textarea.uk-resize-vertical= site.featuredEmbed
fieldset
legend Shing.tv Widget Key
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="shing-channel-slug").uk-form-label Shing.tv Channel Slug
input(id="shing-channel-slug", name="shingChannelSlug", type="text", placeholder="Enter Shing.tv channel slug", value= site.shingChannelSlug).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="shing-widget-key").uk-form-label Shing.tv Widget Key
input(id="shing-widget-key", name="shingWidgetKey", type="text", placeholder="Enter Shing.tv widget key", value= site.shingWidgetKey).uk-input
fieldset
legend Gab links
div(uk-grid).uk-grid-small
@ -85,4 +95,4 @@ block content
label(for="spreaker-url").uk-form-label Spreaker URL
input(id="spreaker-url", name="spreakerUrl", type="url", placeholder="Enter Spreaker URL", value= site.spreakerUrl).uk-input
button(type="submit").uk-button.dtp-button-primary.uk-border-rounded Save Settings
button(type="submit").uk-button.dtp-button-primary Save Settings

@ -0,0 +1,47 @@
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-sites.png`,
siteIcon,
{ aspectRatio: 1 },
)
div(uk-grid).uk-flex-middle
.uk-width-expand
fieldset
legend Default poster
.uk-margin
if postImage
p.uk-card-title Replace your default post image below.
else
p.uk-card-title You do not currently have a default post image. Add one below.
+renderFileUploadImage(
`/admin/settings/images/updatePostImage`,
'site-post-upload',
'site-post-file',
'site-post-picture',
`/img/default-poster.jpg`,
postImage,
{ aspectRatio: 16/9 },
)

@ -0,0 +1,64 @@
mixin renderPostDraftList (posts)
if Array.isArray(posts) && (posts.length > 0)
ul.uk-list.uk-list-divider
each draft in posts
li
a(href=`/post/${draft.slug}`, title="Preview draft")= draft.title
.uk-article-meta
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-expand
.uk-article-meta
div(uk-grid).uk-grid-small.uk-text-small
.uk-width-expand
a(href=`/post/${draft.slug}`, title="Edit draft")= moment(draft.created).fromNow()
if drafts.all
span by
a(href=`/user/${draft.author.username}`)=` ${draft.author.username}`
.uk-width-auto
a(href=`/post/${draft._id}/edit`).uk-display-block
+renderButtonIcon('fa-pen', 'edit')
.uk-width-auto
a(
href="",
title="Delete draft",
data-post-id= draft._id,
data-post-title= draft.title,
onclick="return dtp.app.deletePost(event);",
).uk-text-danger
+renderButtonIcon('fa-trash', 'delete')
else
.uk-margin-small You have no drafts.
mixin renderFullDraftList (posts)
if Array.isArray(posts) && (posts.length > 0)
ul.uk-list.uk-list-divider
each draft in posts
li
a(href=`/post/${draft.slug}`, title="Preview draft")= draft.title
.uk-article-meta
div(uk-grid).uk-grid-medium.uk-flex-middle
.uk-width-expand
.uk-article-meta
div(uk-grid).uk-grid-medium.uk-text-medium
.uk-width-expand
a(href=`/post/${draft.slug}`, title="Edit draft")= moment(draft.created).fromNow()
if drafts.all
span by
a(href=`/user/${draft.author.username}`)=` ${draft.author.username}`
.uk-width-auto
a(href=`/post/${draft._id}/edit`).uk-display-block
+renderButtonIcon('fa-pen', 'edit')
.uk-width-auto
a(
href="",
title="Delete draft",
data-post-id= draft._id,
data-post-title= draft.title,
onclick="return dtp.app.deletePost(event);",
).uk-text-danger
+renderButtonIcon('fa-trash', 'delete')
+renderPaginationBar('/author/posts', drafts.totalPostCount)
else
.uk-margin-small You have no drafts.

@ -0,0 +1,24 @@
mixin renderBlogPostListItem (post, postIndex = 1, postIndexModulus = 3)
a(href=`/post/${post.slug}`).uk-display-block.uk-link-reset
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-1-3@s uk-flex-first", class={
'uk-flex-first@m': ((postIndex % postIndexModulus) === 0),
'uk-flex-last@m': ((postIndex % postIndexModulus) !== 0),
})
if post.image
img(src= `/image/${post.image._id}`).responsive
else
img(src="/img/default-poster.jpg").responsive
div(class='uk-width-1-1 uk-width-2-3@s', class="uk-flex-last", class={
'uk-flex-first@m': ((postIndex % postIndexModulus) !== 0),
'uk-flex-last@m': ((postIndex % postIndexModulus) === 0),
})
article.uk-article
h4(style="line-height: 1.1;").uk-article-title.uk-margin-small= post.title
.uk-article-meta
div(uk-grid).uk-grid-small
.uk-width-auto author: #{post.author.displayName || post.author.username}
.uk-width-auto
span published: #{moment(post.created).format("MMM DD YYYY HH:MM a")}

@ -0,0 +1,70 @@
mixin renderPostList (posts)
if Array.isArray(posts) && (posts.length > 0)
ul.uk-list.uk-list-divider
each post in posts
li
a(href=`/post/${post.slug}`).uk-display-block
div= post.title
.uk-article-meta
div(uk-grid).uk-grid-small.uk-text-small
.uk-width-expand
a(href=`/post/${post.slug}`)= moment(post.created).fromNow()
if posts.all
span by
a(href=`/user/${post.author.username}`)=` ${post.author.username}`
.uk-width-auto
a(href=`/post/${post._id}/edit`).uk-display-block
+renderButtonIcon('fa-pen', 'edit')
.uk-width-auto
a(
href="",
data-post-id= post._id,
data-post-title= post.title,
onclick="return dtp.app.deletePost(event);",
).uk-display-block.uk-text-danger
+renderButtonIcon('fa-trash', 'delete')
div(style="width: 65px;")
span
i.fas.fa-eye
span.uk-margin-small-left= formatCount(post.stats.totalVisitCount)
else
.uk-margin-small There are no posts.
mixin renderPublishedPostList (posts)
if Array.isArray(posts) && (posts.length > 0)
ul.uk-list.uk-list-divider
each post in posts
li
a(href=`/post/${post.slug}`).uk-display-block
div= post.title
.uk-article-meta
div(uk-grid).uk-grid-small.uk-text-small
.uk-width-expand
a(href=`/post/${post.slug}`)= moment(post.created).fromNow()
if posts.all
span by
a(href=`/user/${post.author.username}`)=` ${post.author.username}`
.uk-width-auto
a(href=`/post/${post._id}/edit`).uk-display-block
+renderButtonIcon('fa-pen', 'edit')
.uk-width-auto
a(
href="",
data-post-id= post._id,
data-post-title= post.title,
onclick="return dtp.app.deletePost(event);",
).uk-display-block.uk-text-danger
+renderButtonIcon('fa-trash', 'delete')
div(style="width: 65px;")
span
i.fas.fa-eye
span.uk-margin-small-left= formatCount(post.stats.totalVisitCount)
+renderPaginationBar('/author/posts', published.totalPostCount)
else
.uk-margin-small No published posts.

@ -0,0 +1,28 @@
extends ../../layouts/main
block content
include ../components/draft-list
include ../components/list
include ../../post/components/summary
include ../../components/pagination-bar
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container.uk-container-expand
div(uk-grid).uk-flex-middle
.uk-width-expand
h2.uk-margin-remove Drafts
.uk-width-auto
if user.permissions.canAuthorPosts || user.flags.isAdmin
a(href= "/post/compose").uk-button.uk-button-primary.uk-border-rounded
span
i.fas.fa-plus
span.uk-margin-small-left.uk-text-bold Create Post
.uk-width-medium
a(href= "/author").uk-button.uk-button-primary.uk-border-rounded
span.uk-margin-small-middle.uk-text-bold Author Dashboard
div(uk-grid)
div(class="uk-width-1-1 uk-width-3-3@m")
+renderSectionTitle('Drafts')
+renderFullDraftList(drafts.posts)

@ -3,13 +3,13 @@ block content
include ../components/pagination-bar
include ../post/components/draft-list
include ../post/components/list
include components/draft-list
include components/list
include ../post/components/summary
include ../comment/components/comment
if (user.permissions.canAuthorPosts && user.permissions.canPublishPosts)
if (user.permissions.canAuthorPosts && user.permissions.canPublishPosts || user.flags.isAdmin)
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container.uk-container-expand
div(uk-grid).uk-flex-middle
@ -42,12 +42,20 @@ block content
div(class="uk-width-1-1 uk-width-1-3@m")
.uk-margin
+renderSectionTitle('Drafts')
+renderPostDraftList(drafts)
if (drafts && drafts.length > 0)
+renderSectionTitle('Drafts', { url: '/author/drafts', title: 'See All', label: 'SEE ALL' })
+renderPostDraftList(drafts)
else
+renderSectionTitle('Drafts')
+renderPostDraftList(drafts)
.uk-margin
+renderSectionTitle('Recent Posts', { title: 'View All', label: 'View All', url: '/author/post' })
+renderPostList(published)
if (published && published.length > 0)
+renderSectionTitle('Posts', { url: '/author/posts', title: 'See All', label: 'SEE ALL' })
+renderPostList(published)
else
+renderSectionTitle('Posts')
+renderPostList(published)
else if user.permissions.canAuthorPosts
section.uk-section.uk-section-default.uk-section-xsmall
@ -82,20 +90,33 @@ block content
div(class="uk-width-1-1 uk-width-1-3@m")
.uk-margin
+renderSectionTitle('Drafts')
+renderPostDraftList(drafts.posts)
if (drafts && drafts.length > 0)
+renderSectionTitle('Drafts', { url: '/author/drafts', title: 'See All', label: 'SEE ALL' })
+renderPostDraftList(drafts)
else
+renderSectionTitle('Drafts')
+renderPostDraftList(drafts)
.uk-margin
+renderSectionTitle('Recent Posts', { title: 'View All', label: 'View All', url: '/author/post' })
+renderPostList(published.posts)
if (published && published.length > 0)
+renderSectionTitle('Posts', { url: '/author/posts', title: 'See All', label: 'SEE ALL' })
+renderPostList(published)
else
+renderSectionTitle('Posts')
+renderPostList(published)
else if user.permissions.canPublishPosts || user.flags.isAdmin
else if user.permissions.canPublishPosts
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container.uk-container-expand
div(uk-grid).uk-flex-middle
.uk-width-expand
h1.uk-margin-remove Author Dashboard
.uk-width-auto
a(href= "/author/drafts").uk-button.uk-button-primary.uk-border-rounded
.uk-margin-small-middle.uk-text-bold View Drafts
.uk-width-medium
a(href= "/author/posts").uk-button.uk-button-primary.uk-border-rounded
.uk-margin-small-middle.uk-text-bold View Posts
.uk-margin
div(class="uk-width-1-1 uk-width-3-3@m")
.uk-margin

@ -1,58 +1,27 @@
extends ../../layouts/main
block content
include ../../post/components/draft-list
include ../../post/components/list
include ../components/draft-list
include ../components/list
include ../../post/components/summary
include ../../components/pagination-bar
section.uk-section.uk-section-default.uk-section-small
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container.uk-container-expand
h1 Post Author Dashboard
div(uk-grid).uk-flex-middle
.uk-width-expand
h2.uk-margin-remove Published Posts
if user.permissions.canAuthorPosts
.uk-width-auto
a(href= "/post/compose").uk-button.uk-button-primary.uk-border-rounded
span
i.fas.fa-plus
span.uk-margin-small-left.uk-text-bold Create Post
.uk-width-medium
a(href= "/author").uk-button.uk-button-primary.uk-border-rounded
span.uk-margin-small-left.uk-text-bold Author Dashboard
div(uk-grid)
.uk-width-2-3
.uk-margin
+renderSectionTitle('Your Posts')
.content-block
if published && Array.isArray(published.posts) && (published.posts.length > 0)
.uk-margin
ul.uk-list.uk-list-divider
each post in published.posts
li
a(href=`/post/${post.slug}`).uk-display-block
div= post.title
.uk-article-meta
div(uk-grid).uk-grid-small.uk-text-small
.uk-width-expand
a(href=`/post/${post.slug}`)= moment(post.created).fromNow()
.uk-width-auto
a(href=`/post/${post._id}/edit`).uk-display-block
+renderButtonIcon('fa-pen', 'edit')
.uk-width-auto
a(
href="",
data-post-id= post._id,
data-post-title= post.title,
onclick="return dtp.app.deletePost(event);",
).uk-display-block.uk-text-danger
+renderButtonIcon('fa-trash', 'delete')
div(style="width: 65px;")
span
i.fas.fa-eye
span.uk-margin-small-left= formatCount(post.stats.totalVisitCount)
+renderPaginationBar('/author/post', published.totalPostCount)
else
div You have no published posts.
.uk-width-1-3
.uk-margin
+renderSectionTitle('Your Drafts')
+renderPostDraftList(drafts.posts)
.uk-margin
+renderSectionTitle('Archived')
+renderPostList(archive.posts)
div(class="uk-width-1-1 uk-width-3-3@m")
+renderSectionTitle('Posts')
+renderPublishedPostList(published.posts)

@ -52,7 +52,7 @@ mixin renderCell (label, value, className)
mixin renderBackButton (options)
- options = Object.assign({ includeLabel: true, label: 'Back' }, options)
button(type="button", onclick="window.history.back();").uk-button.uk-button-default.uk-border-rounded
button(type="button", onclick="window.history.back();").uk-button.uk-button-default
span
i.fas.fa-chevron-left
if options.includeLabel

@ -32,12 +32,21 @@ mixin renderMenuItem (iconClass, label)
li(class={ "uk-active": (currentView === 'announcement') })
a(href='/announcement').uk-display-block
+renderMenuItem('fa-bullhorn', 'Announcements')
li(class={ "uk-active": (currentView === 'authors') })
a(href=`/post/authors`).uk-display-block
div(uk-grid).uk-grid-collapse
.uk-width-auto
.app-menu-icon
i.fas.fa-user
.uk-width-expand All Authors
each menuItem in mainMenu
if Array.isArray(mainMenu)
li.uk-nav-header Pages
li(class={ 'uk-active': (pageSlug === menuItem.slug) })
a(href= menuItem.url, title= menuItem.label)
+renderMenuItem(menuItem.icon || 'fa-file', menuItem.label)
each menuItem in mainMenu
li(class={ 'uk-active': (pageSlug === menuItem.slug) })
a(href= menuItem.url, title= menuItem.label)
+renderMenuItem(menuItem.icon || 'fa-file', menuItem.label)
if user
li.uk-nav-header Member Menu

@ -3,6 +3,7 @@ include ../newsroom/components/feed-entry-list-item
include ../venue/components/channel-card
include ../venue/components/channel-list-item
include ../post/components/author-credit
- var isLive = !!shingChannelStatus && shingChannelStatus.isLive && !!shingChannelStatus.liveEpisode;
@ -116,6 +117,13 @@ mixin renderPageSidebar ( )
+renderNewsroomFeedEntryListItem(entry)
//-
//- Author credit
//-
if author && posts.length > 0
.uk-card.uk-card-default.uk-card-small
.uk-card-body
+renderPostAuthorCredit(author)
//-
//- Newsletter Signup
//-
div(uk-sticky={ offset: 60, bottom: '#dtp-content-grid' }, style="z-index: initial;").uk-margin-medium

@ -1,7 +1,7 @@
block facebook-card
meta(property='og:site_name', content= site.name)
meta(property='og:type', content='website')
meta(property='og:image', content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`)
meta(property='og:image', content= shareImage || `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`)
meta(property='og:url', content= `https://${site.domain}${request.url}`)
meta(property='og:title', content= pageTitle || site.name)
meta(property='og:description', content= pageDescription || site.description)

@ -1,5 +1,5 @@
block twitter-card
meta(name='twitter:card', content='summary_large_image')
meta(name='twitter:image' content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`)
meta(name='twitter:image' content= shareImage || `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`)
meta(name='twitter:title', content= pageTitle || site.name)
meta(name='twitter:description', content= pageDescription || site.description)

@ -118,4 +118,8 @@ html(lang='en')
else
script(src=`/dist/js/dtpsites-admin.min.js?v=${pkg.version}`, type="module")
block viewjs
block viewjs
script.
window.addEventListener('dtp-load', function () {
return dtp.app.updatePostTimestamps();
});

@ -6,13 +6,7 @@ block content
section.uk-section.uk-section-default.uk-section-small
.uk-container
.uk-margin
div(uk-grid).uk-flex-middle
.uk-width-expand
h1.uk-margin-remove #{site.name} Newsroom
.uk-width-auto
a(href="/newsroom/feed").uk-button.dtp-button-primary.uk-button-small.uk-border-rounded View All
h1 #{site.name} Newsroom
if Array.isArray(newsroom.feeds) && (newsroom.feeds.length > 0)
div(uk-grid).uk-grid-match
each feed in newsroom.feeds

@ -1,27 +0,0 @@
extends ../layouts/main
block content
include ../components/pagination-bar
section.uk-section.uk-section-default
.uk-container
article.uk-article
.uk-margin
h1.uk-article-title.uk-margin-remove #{site.name} News Feed
.uk-text-bold #{formatCount(newsroom.totalFeedEntryCount)} articles indexed by #{site.name} in one chronological feed.
.uk-margin
if Array.isArray(newsroom.entries) && (newsroom.entries.length > 0)
ul.uk-list.uk-list-divider
each entry in newsroom.entries
li
.uk-text-large.uk-text-bold.uk-margin-small
a(href= entry.link, target="shing_reader")= entry.title
.uk-margin-small= entry.description
.uk-text-small source: #[a(href= entry.feed.link, target="_blank")= entry.feed.title]
else
div There are no news feed entries.
.uk-margin
+renderPaginationBar(`/newsroom/feed`, newsroom.totalFeedEntryCount)

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

@ -0,0 +1,16 @@
extends ../../layouts/main-sidebar
block content
include ../../components/pagination-bar
include components/credit
div(uk-grid).uk-flex-expand
.uk-width-expand
h3.uk-margin-remove= `Author List`
if Array.isArray(authors) && (authors.length > 0)
ul.uk-list.uk-list-divider
each author in authors
li
+renderAuthorCredit(author)
.uk-card-footer
+renderPaginationBar(`/post/authors`, totalAuthorCount )

@ -0,0 +1,18 @@
mixin renderAuthorCredit (author)
div(uk-grid).uk-grid-small
.uk-width-auto
+renderProfileIcon(author)
.uk-width-expand
.uk-margin-small
div(uk-grid).uk-flex-middle
.uk-width-expand
- var userUrl = !!author.coreUserId ? `/user/core/${author._id}` : `/user/${author.username}`;
a(href= userUrl, title="View member profile")
.uk-text-bold(style="line-height: 1em;")= author.displayName || author.username
.uk-width-auto
.uk-text-small.uk-text-muted(style="line-height: 1em;")
if author.coreUserId
a(href=`${process.env.DTP_CORE_AUTH_SCHEME}://${author.core.meta.domain}/user/${author.coreUserId}`)= author.core.meta.name
else if !Array.isArray(posts)
a(href= `/post/author/${author.username}`)= `View posts by author`
.uk-text-small= author.bio

@ -0,0 +1,15 @@
mixin renderPostSummaryFull (post)
div(uk-grid).uk-grid-small
if post.image
.uk-width-auto
img(src= `/image/${post.image._id}`).uk-width-small
else
.uk-width-auto
img(src="/img/default-poster.jpg").uk-width-small
.uk-width-expand
.uk-text-large.uk-text-bold(style="line-height: 1em;")
a(href=`/post/${post.slug}`)= `${post.title}`
.uk-text-small.uk-text-muted
div
div= moment(post.created).fromNow()
div= post.summary

@ -0,0 +1,16 @@
extends ../../layouts/main-sidebar
block content
include ../../components/pagination-bar
include components/list
div(uk-grid).uk-flex-expand
.uk-width-expand
h3.uk-margin-remove= `Posts by ${author.username}`
if Array.isArray(posts) && (posts.length > 0)
ul.uk-list.uk-list-divider
each post in posts
li
+renderPostSummaryFull(post)
.uk-card-footer
+renderPaginationBar(`/post/author/${author.username}`, posts.length )

@ -6,13 +6,13 @@ mixin renderPostAuthorCredit (author)
.uk-margin-small
div(uk-grid).uk-flex-middle
.uk-width-expand
- var userUrl = !!author.coreUserId ? `/user/core/${author._id}` : `/user/${author._id}`;
- var userUrl = !!author.coreUserId ? `/user/core/${author._id}` : `/user/${author.username}`;
a(href= userUrl, title="View member profile")
.uk-text-bold(style="line-height: 1em;")= author.displayName || author.username
.uk-width-auto
.uk-text-small.uk-text-muted(style="line-height: 1em;")
if author.coreUserId
a(href=`${process.env.DTP_CORE_AUTH_SCHEME}://${author.core.meta.domain}/user/${author.coreUserId}`)= author.core.meta.name
else
a(href= "/")= site.name
else if !Array.isArray(posts)
a(href= `/post/author/${author.username}`)= `View posts by author`
.uk-text-small= author.bio

@ -21,4 +21,5 @@ mixin renderBlogPostListItem (post, postIndex = 1, postIndexModulus = 3)
div(uk-grid).uk-grid-small
.uk-width-auto author: #{post.author.displayName || post.author.username}
.uk-width-auto
span published: #{moment(post.created).format("MMM DD YYYY HH:MM a")}
span published:
span(data-dtp-timestamp= post.created)

@ -31,4 +31,4 @@ mixin renderPostList (posts)
i.fas.fa-eye
span.uk-margin-small-left= formatCount(post.stats.totalVisitCount)
else
div You have authored posts.
div You have no authored posts.

@ -33,12 +33,15 @@ block content
}
input(id="slug", name="slug", type="text", placeholder= "Enter post URL slug", value= post ? postSlug : undefined).uk-input
.uk-text-small The slug is used in the link to the page https://#{site.domain}/post/#{post ? post.slug : 'your-slug-here'}
.uk-margin
label(for="tags").uk-form-label Post tags
input(id="tags", name="tags", placeholder= "Enter a comma-separated list of tags", value= (post.tags || [ ]).join(', ')).uk-input
.uk-margin
label(for="summary").uk-form-label Post summary
textarea(id="summary", name="summary", rows="4", placeholder= "Enter post summary (text only, no HTML)").uk-textarea= post ? post.summary : undefined
div(uk-grid)
.uk-width-auto
button(type="submit").uk-button.uk-button-primary= post ? 'Update post' : 'Create post'
button(type="submit").uk-button.uk-button-primary= 'Update post'
.uk-margin
label(for="status").uk-form-label Status
select(id="status", name="status").uk-select

@ -24,16 +24,25 @@ block content
div(uk-grid)
.uk-width-auto
+renderGabShareButton(`https://${site.domainKey}/post/${post.slug}`, `${post.title} - ${post.summary}`)
+renderSectionTitle('Post tags')
if Array.isArray(post.tags) && (post.tags.length > 0)
div(uk-grid).uk-grid-small
each tag in post.tags
-
var tagSlug;
tagSlug = tag.replace(" ", "_")
a(href=`/tag/${tagSlug}`).uk-display-block.uk-link-reset.uk-margin-small= tag
.uk-margin
.uk-article-meta
div(uk-grid).uk-grid-small.uk-flex-top
.uk-width-expand
div #{moment(post.created).format('MMM DD, YYYY, hh:mm a')}, by #[a(href=`/user/${post.author._id}`)= post.author.displayName || post.author.username]
uk-text-small(data-dtp-timestamp= post.created)
span by #[a(href=`/user/${post.author.username}`)= post.author.displayName || post.author.username]
if user && user.hasAuthorDashboard
.uk-width-auto= post.status
if post.author._id.equals(user._id) || user.permissions.canPublishPosts
if post.author._id.equals(user._id) || user.hasAuthorDashboard
.uk-width-auto
a(href=`/post/${post._id}/edit`).uk-display-block
+renderButtonIcon('fa-pen', 'edit')
@ -59,7 +68,9 @@ block content
if post.updated
.uk-margin
.uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}.
.uk-article-meta This post was updated on
uk-text-small(data-dtp-timestamp= post.updated)
.content-block
.uk-margin

@ -0,0 +1,19 @@
mixin renderPostSummaryFull (post)
div(uk-grid).uk-grid-small
if post.image
.uk-width-auto
img(src= `/image/${post.image._id}`).uk-width-medium
else
.uk-width-auto
img(src="/img/default-poster.jpg").uk-width-medium
.uk-width-expand
.uk-text-large.uk-text-bold(style="line-height: 1em;")
a(href=`/post/${post.slug}`)= `${post.title}`
.uk-text-small.uk-text-muted
div
div= moment(post.created).fromNow()
span by
a(href=`/user/${post.author.username}`)=` ${post.author.username}`
if user && allPosts
div= `Status: ${post.status}`
div= post.summary

@ -0,0 +1,37 @@
extends ../layouts/main-sidebar
include components/list
include ../components/pagination-bar
block content
if Array.isArray(posts) && (posts.length > 0)
h3= `Posts with the tag ${tag}.`
ul.uk-list.uk-list-divider
each post in posts
li
+renderPostSummaryFull(post)
.uk-card-footer
+renderPaginationBar(`/tag/${tagSlug}`, posts.length )
//- li
if post.image
img(src= `/image/${post.image._id}`, href=`/post/${post.slug}`, style="max-height: 350px; object-fit: cover; vertical-align:middle;margin:0px 20px;").responsive
else
img(src="/img/default-poster.jpg", href=`/post/${post.slug}`, style="max-height: 350px; object-fit: cover; vertical-align:middle;margin:0px 20px;").responsive
a(href=`/post/${post.slug}`).uk-display-block
div.h2= post.title
.uk-article-meta
div(uk-grid).uk-grid-small.uk-text-small
.uk-width-expand
a(href=`/post/${post.slug}`)= moment(post.created).fromNow()
span by
a(href=`/user/${post.author.username}`)=` ${post.author.username}`
.uk-width-expand
div= post.summary
else
h3= `There are no posts with the tag ${tag}.`

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

@ -13,24 +13,13 @@ block content
div(uk-grid).uk-flex-center
div(class="uk-width-1-1 uk-width-1-3@m")
.uk-margin-small
a(href="/auth/core").uk-button.uk-button-primary.uk-border-rounded
span
i.fas.fa-plug
span.uk-margin-small-left DTP Connect
a(href="/auth/core").uk-button.uk-button-primary.uk-border-rounded DTP Connect
.uk-text-small Connect using DTP Core
div(class="uk-width-1-1 uk-width-1-3@m")
.uk-margin-small
a(href="/welcome/signup").uk-button.uk-button-secondary.uk-border-rounded
span
i.fas.fa-user-plus
span.uk-margin-small-left Create Account
a(href="/welcome/signup").uk-button.uk-button-secondary.uk-border-rounded Create Account
.uk-text-small Create a local account
div(class="uk-width-1-1 uk-width-1-3@m")
.uk-margin-small
a(href="/welcome/login").uk-button.uk-button-default.uk-border-rounded
span
i.fas.fa-door-open
span.uk-margin-small-left Sign In
a(href="/welcome/login").uk-button.uk-button-default.uk-border-rounded Sign In
.uk-text-small Log in with your local account

@ -32,4 +32,4 @@ block content
.uk-width-auto
a(href="/").uk-text-muted Forgot password
.uk-width-auto
button(type="submit").uk-button.dtp-button-primary.uk-border-rounded Login
button(type="submit").uk-button.dtp-button-primary Login

@ -56,4 +56,4 @@ block content
.uk-width-expand
+renderBackButton()
.uk-width-auto
button(type="submit").uk-button.uk-button-primary.uk-border-rounded Create Account
button(type="submit").uk-button.uk-button-primary Create Account

@ -128,7 +128,7 @@ class StickerIngestJob extends SiteWorkerProcess {
throw new Error(`unsupported sticker type: ${job.data.sticker.original.type}`);
}
this.jobLog(job, 'fetching original media', {
this.jobLog(job, 'fetching original media', { // blah 62096adcb69874552a0f87bc
stickerId: job.data.sticker._id,
slug: job.data.sticker.slug,
type: job.data.sticker.original.type,

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@ -28,9 +28,7 @@ window.addEventListener('load', async ( ) => {
// service worker
if ('serviceWorker' in navigator) {
try {
dtp.registration = await navigator.serviceWorker.register('/dist/js/service_worker.min.js', {
scope: '/',
});
dtp.registration = await navigator.serviceWorker.register('/dist/js/service_worker.min.js');
dtp.log.info('load', 'service worker startup complete', { scope: dtp.registration.scope });
} catch (error) {
console.log('service worker startup failed', { error });

@ -404,6 +404,7 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
try {
await UIkit.modal.confirm(`Are you sure you want to remove post "${postTitle}"?`);
} catch (error) {
// canceled
return false;
}
@ -428,6 +429,7 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
try {
await UIkit.modal.confirm(`Are you sure you want to remove page "${pageTitle}"?`);
} catch (error) {
// canceled
return false;
}
@ -451,6 +453,7 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
try {
await UIkit.modal.confirm(`Are you sure you want to remove site link "${link.label}"?`);
} catch (error) {
// canceled
return false;
}
@ -474,6 +477,7 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
try {
await UIkit.modal.confirm(`Are you sure you want to remove channel "${channel.name}"?`);
} catch (error) {
// canceled
return false;
}
@ -483,29 +487,45 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
} catch (error) {
UIkit.modal.alert(`Failed to remove site link: ${error.message}`);
}
return false;
}
async disconnectCore (event) {
async submitImageForm (event) {
event.preventDefault();
event.stopPropagation();
const target = event.currentTarget || event.target;
const core = JSON.parse(target.getAttribute('data-core'));
try {
await UIkit.modal.confirm(`Are you sure you want to disconnect from Core "${core.name}"?`);
} catch (error) {
return false;
}
try {
const response = await fetch(`/admin/core-node/${core._id}`, { method: 'DELETE' });
await this.processResponse(response);
} catch (error) {
UIkit.modal.alert(`Failed to disconnect from Core: ${error.message}`);
}
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 false;
return;
}
}

@ -546,6 +546,17 @@ export default class DtpSiteApp extends DtpApp {
return false;
}
updatePostTimestamps () {
const timestamps = document.querySelectorAll('[data-dtp-timestamp]');
// console.log(timestamps);
timestamps.forEach((timestamp) => {
const postTime = timestamp.getAttribute('data-dtp-timestamp');
const format = timestamp.getAttribute('data-dtp-time-format');
timestamp.textContent = moment(postTime).format(format || 'MMM DD, YYYY, hh:mm a');
});
}
}
dtp.DtpSiteApp = DtpSiteApp;

@ -97,9 +97,7 @@ button.uk-button.dtp-button-subscribe {
}
a.uk-button.dtp-button-default,
a.uk-button.uk-button-default,
button.uk-button.dtp-button-default,
button.uk-button.uk-button-default {
button.uk-button.dtp-button-default {
background: none;
outline: none;
border: solid 2px rgb(75, 75, 75);
@ -109,54 +107,47 @@ button.uk-button.uk-button-default {
&:hover {
background-color: rgb(75, 75, 75);
color: #e8e8e8;
}
}
a.uk-button.dtp-button-primary,
a.uk-button.uk-button-primary,
button.uk-button.dtp-button-primary,
button.uk-button.uk-button-primary {
button.uk-button.dtp-button-primary {
background: none;
outline: none;
border: solid 2px #1e87f0;
color: @button-label-color;
color: #c8c8c8;
transition: background-color 0.2s;
&:hover {
background-color: #1e87f0;
color: #e8e8e8;
}
}
a.uk-button.dtp-button-secondary,
a.uk-button.uk-button-secondary,
button.uk-button.dtp-button-secondary,
button.uk-button.uk-button-secondary {
button.uk-button.dtp-button-secondary {
background: none;
outline: none;
border: solid 2px rgb(160,160,160);
color: @button-label-color;
border: solid 2px rgb(75, 75, 75);
color: #c8c8c8;
&:hover {
background-color: rgb(160,160,160);
color: #e8e8e8;
background-color: rgb(75, 75, 75);
}
}
a.uk-button.dtp-button-danger,
a.uk-button.uk-button-danger,
button.uk-button.dtp-button-danger,
button.uk-button.uk-button-danger {
button.uk-button.dtp-button-danger {
background: none;
outline: none;
border: solid 2px rgb(255, 0, 0);
color: @button-label-color;
color: @global-color;
&:hover {
background-color: rgb(255, 0, 0);
color: #e8e8e8;
color: #ffffff;
}
}

@ -330,6 +330,11 @@ module.exports = {
expire: ONE_MINUTE,
message: 'You are reading posts too quickly',
},
getAllAuthorsView: {
total: 20,
expire: ONE_MINUTE,
message: 'You are loading pages too quickly',
},
getIndex: {
total: 60,
expire: ONE_MINUTE,

@ -0,0 +1,7 @@
#!/bin/sh
git pull prod master
yarn --production=false
gulp build
./restart-production

@ -1,178 +0,0 @@
// dtp-media-engine.js
// Copyright (C) 2022 DTP Technologies, LLC
// All Rights Reserved
'use strict';
require('dotenv').config();
const path = require('path');
const mongoose = require('mongoose');
const mediasoup = require('mediasoup');
const { SiteAsync, SiteCommon, SitePlatform, SiteLog } = require(path.join(__dirname, 'lib', 'site-lib'));
module.rootPath = __dirname;
module.pkg = require(path.join(module.rootPath, 'package.json'));
module.config = {
component: { name: 'dtpMediaEngine', slug: 'dtp-media-engine' },
root: module.rootPath,
site: require(path.join(module.rootPath, 'config', 'site')),
webRtcServer: [
{
protocol: 'udp',
ip: process.env.MEDIASOUP_WEBRTC_BIND_ADDR || '127.0.0.1',
port: process.env.MEDIASOUP_WEBRTC_BIND_PORT || 20000,
}
]
};
module.log = new SiteLog(module, module.config.component);
class MediaEngineWorker extends SiteCommon {
constructor ( ) {
super(module, { name: 'dtpMediaWorker', slug: 'dtp-media-worker' });
this._id = mongoose.Types.ObjectId();
}
async start ( ) {
await super.start();
try {
this.worker = await mediasoup.createWorker({
logLevel: 'warn',
dtlsCertificateFile: process.env.HTTPS_SSL_CRT,
dtlsPrivateKeyFile: process.env.HTTPS_SSL_KEY,
});
} catch (error) {
throw new Error(`failed to start mediasoup worker process: ${error.message}`);
}
try {
const BIND_PORT = 20000 + module.nextWorkerIdx++;
this.webRtcServer = await this.worker.createWebRtcServer({
listenInfos: [
{
protocol: 'udp',
ip: '127.0.0.1',
port: BIND_PORT,
},
{
protocol: 'tcp',
ip: '127.0.0.1',
port: BIND_PORT,
},
],
});
} catch (error) {
throw new Error(`failed to start mediasoup WebRTC Server: ${error.message}`);
}
}
async stop ( ) {
if (this.webRtcServer && !this.webRtcServer.closed) {
this.log.info('closing mediasoup WebRTC server');
this.webRtcServer.close();
delete this.webRtcServer;
}
if (this.worker && !this.worker.closed) {
this.log.info('closing mediasoup worker process');
this.worker.close();
delete this.worker;
}
await super.stop();
}
}
module.onNewWorker = async (worker) => {
module.log.info('new worker created', { worker: worker.pid });
worker.observer.on('close', ( ) => {
module.log.info('worker shutting down', { worker: worker.pid });
});
worker.observer.on('newrouter', (router) => {
module.log.info('new router created', { worker: worker.pid, router: router.id });
router.observer.on('close', ( ) => {
module.log.info('router shutting down', { worker: worker.pid, router: router.id });
});
});
};
module.createWorker = async ( ) => {
const worker = new MediaEngineWorker();
module.workers.push(worker);
await worker.start();
};
module.shutdown = async ( ) => {
await SiteAsync.each(module.workers, async (worker) => {
try {
await worker.stop();
} catch (error) {
module.log.error('failed to stop worker', { error });
}
});
};
/*
* SERVER PROCESS INIT
*/
(async ( ) => {
process.on('unhandledRejection', (error, p) => {
module.log.error('Unhandled rejection', {
error: error,
promise: p,
stack: error.stack
});
});
process.on('warning', (error) => {
module.log.alert('warning', { error });
});
process.once('SIGINT', async ( ) => {
module.log.info('SIGINT received');
module.log.info('requesting shutdown...');
await module.shutdown();
const exitCode = await SitePlatform.shutdown();
process.nextTick(( ) => {
process.exit(exitCode);
});
});
process.once('SIGUSR2', async ( ) => {
await SitePlatform.shutdown();
process.kill(process.pid, 'SIGUSR2');
});
try {
await SitePlatform.startPlatform(module);
} catch (error) {
module.log.error(`failed to start DTP ${module.config.component.slug} process`, { error });
return;
}
try {
module.log.info('registering mediasoup observer callbacks');
mediasoup.observer.on('newworker', module.onNewWorker);
module.log.info('creating mediasoup worker instance');
module.nextWorkerIdx = 0;
module.workers = [ ];
await module.createWorker();
module.log.info('DTP Media Engine online');
} catch (error) {
module.log.error('failed to start DTP Media Engine', { error });
process.exit(-1);
}
})();

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save