diff --git a/.gitignore b/.gitignore index 95f99b0..3868498 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ ssl/*crt ssl/*key data/minio -data/minio.old node_modules dist start-local-* \ No newline at end of file diff --git a/app/controllers/admin.js b/app/controllers/admin.js index 3800970..809feae 100644 --- a/app/controllers/admin.js +++ b/app/controllers/admin.js @@ -51,6 +51,7 @@ class AdminController extends SiteController { router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log'))); router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter'))); router.use('/newsroom', await this.loadChild(path.join(__dirname, 'admin', 'newsroom'))); + router.use('/otp', await this.loadChild(path.join(__dirname, 'admin', 'otp'))); router.use('/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'); } diff --git a/app/controllers/admin/core-node.js b/app/controllers/admin/core-node.js index fd7cdc9..2ae9816 100644 --- a/app/controllers/admin/core-node.js +++ b/app/controllers/admin/core-node.js @@ -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 = { diff --git a/app/controllers/admin/otp.js b/app/controllers/admin/otp.js new file mode 100644 index 0000000..0363aa9 --- /dev/null +++ b/app/controllers/admin/otp.js @@ -0,0 +1,55 @@ +// admin/otp.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const express = require('express'); +// const multer = require('multer'); + +const { SiteController, SiteError } = require('../../../lib/site-lib'); + +class OtpAdminController extends SiteController { + + constructor (dtp) { + super(dtp, module.exports); + } + + async start ( ) { + // const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${module.exports.slug}` }); + + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'otp'; + return next(); + }); + + // router.param('otp', this.populateOtp.bind(this)); + + router.get('/', this.getIndex.bind(this)); + + return router; + } + + async getIndex (req, res, next) { + try { + const { otpAuth: otpAuthService } = this.dtp.services; + if (!req.user) { + throw new SiteError(402, "Error getting user"); + } + res.locals.tokens = await otpAuthService.getBackupTokens(req.user, "Admin"); + res.render('admin/otp/index'); + } catch (error) { + this.log.error('failed to get tokens', { error }); + return next(error); + } + } + +} + +module.exports = { + name: 'adminOtp', + slug: 'admin-opt', + create: async (dtp) => { return new OtpAdminController(dtp); }, +}; \ No newline at end of file diff --git a/app/controllers/admin/post.js b/app/controllers/admin/post.js index d412d95..0c6b41f 100644 --- a/app/controllers/admin/post.js +++ b/app/controllers/admin/post.js @@ -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'); } diff --git a/app/controllers/admin/settings.js b/app/controllers/admin/settings.js index e5eefcb..87b0ab1 100644 --- a/app/controllers/admin/settings.js +++ b/app/controllers/admin/settings.js @@ -16,6 +16,13 @@ class SettingsController extends SiteController { async start ( ) { const router = express.Router(); + + const imageUpload = this.createMulter('uploads', { + limits: { + fileSize: 1024 * 1000 * 12, + }, + }); + router.use(async (req, res, next) => { res.locals.currentView = 'admin'; res.locals.adminView = 'settings'; @@ -23,9 +30,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 = { diff --git a/app/controllers/author.js b/app/controllers/author.js index 07b6696..f3f6983 100644 --- a/app/controllers/author.js +++ b/app/controllers/author.js @@ -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; } diff --git a/app/controllers/newsroom.js b/app/controllers/newsroom.js index c2bbfb9..b97d8d3 100644 --- a/app/controllers/newsroom.js +++ b/app/controllers/newsroom.js @@ -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 { diff --git a/app/controllers/page.js b/app/controllers/page.js index 9cd05d0..b99bc34 100644 --- a/app/controllers/page.js +++ b/app/controllers/page.js @@ -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}`; diff --git a/app/controllers/post.js b/app/controllers/post.js index faddb5f..acaab3c 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -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) { diff --git a/app/controllers/tag.js b/app/controllers/tag.js new file mode 100644 index 0000000..485555b --- /dev/null +++ b/app/controllers/tag.js @@ -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; + }, +}; \ No newline at end of file diff --git a/app/models/kaleidoscope-event.js b/app/models/kaleidoscope-event.js index b76f33b..c24e98e 100644 --- a/app/models/kaleidoscope-event.js +++ b/app/models/kaleidoscope-event.js @@ -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 }, diff --git a/app/models/lib/user-types.js b/app/models/lib/user-types.js index 94c8d60..7d7d7d3 100644 --- a/app/models/lib/user-types.js +++ b/app/models/lib/user-types.js @@ -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 }); \ No newline at end of file +}); \ No newline at end of file diff --git a/app/models/media-router.js b/app/models/media-router.js deleted file mode 100644 index c8dc32c..0000000 --- a/app/models/media-router.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/app/models/media-worker.js b/app/models/media-worker.js deleted file mode 100644 index feebc87..0000000 --- a/app/models/media-worker.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/app/models/post.js b/app/models/post.js index 736338f..3ba931a 100644 --- a/app/models/post.js +++ b/app/models/post.js @@ -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 }, diff --git a/app/models/user-block.js b/app/models/user-block.js index f6396e1..2046267 100644 --- a/app/models/user-block.js +++ b/app/models/user-block.js @@ -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); \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index 5edef6c..730f312 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -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); diff --git a/app/services/core-node.js b/app/services/core-node.js index 995f9c3..923fd37 100644 --- a/app/services/core-node.js +++ b/app/services/core-node.js @@ -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 = { diff --git a/app/services/hive.js b/app/services/hive.js index b6366a7..3b3007d 100644 --- a/app/services/hive.js +++ b/app/services/hive.js @@ -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 = { diff --git a/app/services/image.js b/app/services/image.js index ea46e51..560106e 100644 --- a/app/services/image.js +++ b/app/services/image.js @@ -16,7 +16,7 @@ const { SiteService, SiteAsync } = require('../../lib/site-lib'); class ImageService extends SiteService { - constructor (dtp) { + constructor(dtp) { super(dtp, module.exports); this.populateImage = [ { @@ -26,20 +26,20 @@ class ImageService extends SiteService { ]; } - async start ( ) { + async start() { await super.start(); await fs.promises.mkdir(process.env.DTP_IMAGE_WORK_PATH, { recursive: true }); } - async create (owner, imageDefinition, file) { + async create(owner, imageDefinition, file) { const NOW = new Date(); const { minio: minioService } = this.dtp.services; try { this.log.debug('processing uploaded image', { imageDefinition, file }); - - const sharpImage = await sharp(file.path); + + const sharpImage = sharp(file.path); const metadata = await sharpImage.metadata(); - + // create an Image model instance, but leave it here in application memory. // we don't persist it to the db until MinIO accepts the binary data. const image = new SiteImage(); @@ -49,12 +49,12 @@ class ImageService extends SiteService { image.size = file.size; image.file.bucket = process.env.MINIO_IMAGE_BUCKET; image.metadata = this.makeImageMetadata(metadata); - + const imageId = image._id.toString(); const ownerId = owner._id.toString(); const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`; image.file.key = fileKey; - + // upload the image file to MinIO const response = await minioService.uploadFile({ bucket: image.file.bucket, @@ -65,13 +65,13 @@ class ImageService extends SiteService { 'Content-Length': file.size, }, }); - + // store the eTag from MinIO in the Image model image.file.etag = response.etag; - + // save the Image model to the db await image.save(); - + this.log.info('processed uploaded image', { ownerId, imageId, fileKey }); return image.toObject(); } catch (error) { @@ -83,14 +83,14 @@ class ImageService extends SiteService { } } - async getImageById (imageId) { + async getImageById(imageId) { const image = await SiteImage .findById(imageId) .populate(this.populateImage); return image; } - async getRecentImagesForOwner (owner) { + async getRecentImagesForOwner(owner) { const images = await SiteImage .find({ owner: owner._id }) .sort({ created: -1 }) @@ -100,7 +100,7 @@ class ImageService extends SiteService { return images; } - async deleteImage (image) { + async deleteImage(image) { const { minio: minioService } = this.dtp.services; this.log.debug('removing image from storage', { bucket: image.file.bucket, key: image.file.key }); @@ -110,13 +110,13 @@ class ImageService extends SiteService { await SiteImage.deleteOne({ _id: image._id }); } - async processImageFile (owner, file, outputs, options) { + async processImageFile(owner, file, outputs, options) { this.log.debug('processing image file', { owner, file, outputs }); const sharpImage = sharp(file.path); return this.processImage(owner, sharpImage, outputs, options); } - async processImage (owner, sharpImage, outputs, options) { + async processImage(owner, sharpImage, outputs, options) { const NOW = new Date(); const service = this; const { minio: minioService } = this.dtp.services; @@ -128,7 +128,7 @@ class ImageService extends SiteService { const imageWorkPath = process.env.DTP_IMAGE_WORK_PATH || '/tmp'; const metadata = await sharpImage.metadata(); - async function processOutputImage (output) { + async function processOutputImage(output) { const outputMetadata = service.makeImageMetadata(metadata); outputMetadata.width = output.width; outputMetadata.height = output.height; @@ -149,7 +149,7 @@ class ImageService extends SiteService { height: output.height, options: output.resizeOptions, }) - ; + ; chain = chain[output.format](output.formatParameters); output.filePath = path.join(imageWorkPath, `${image._id}.${output.width}x${output.height}.${output.format}`); @@ -165,11 +165,11 @@ class ImageService extends SiteService { const imageId = image._id.toString(); const ownerId = owner._id.toString(); const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/images/${imageId.slice(0, 3)}/${imageId}.${output.format}`; - + image.file.bucket = process.env.MINIO_IMAGE_BUCKET; image.file.key = fileKey; image.size = output.stat.size; - + // upload the image file to MinIO const response = await minioService.uploadFile({ bucket: image.file.bucket, @@ -180,13 +180,13 @@ class ImageService extends SiteService { 'Content-Length': output.stat.size, }, }); - + // store the eTag from MinIO in the Image model image.file.etag = response.etag; - + // save the Image model to the db await image.save(); - + service.log.info('processed uploaded image', { ownerId, imageId, fileKey }); if (options.removeWorkFiles) { @@ -216,7 +216,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, diff --git a/app/services/oauth2.js b/app/services/oauth2.js index 4608d05..2e886b8 100644 --- a/app/services/oauth2.js +++ b/app/services/oauth2.js @@ -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 }); } diff --git a/app/services/otp-auth.js b/app/services/otp-auth.js index 85fb0b4..c919c33 100644 --- a/app/services/otp-auth.js +++ b/app/services/otp-auth.js @@ -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; } } diff --git a/app/services/page.js b/app/services/page.js index c460b3e..24ea1ca 100644 --- a/app/services/page.js +++ b/app/services/page.js @@ -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()); diff --git a/app/services/post.js b/app/services/post.js index d2e1b8c..0e6a7da 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -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; } diff --git a/app/services/resource.js b/app/services/resource.js index a4f6d84..c0d787b 100644 --- a/app/services/resource.js +++ b/app/services/resource.js @@ -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()}`; } diff --git a/app/services/user.js b/app/services/user.js index afba7f6..3d90328 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -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 }, ); } diff --git a/app/services/venue.js b/app/services/venue.js index d70d57a..9afdcc7 100644 --- a/app/services/venue.js +++ b/app/services/venue.js @@ -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(); } }; diff --git a/app/views/admin/announcement/index.pug b/app/views/admin/announcement/index.pug index 3c242ba..0e421ca 100644 --- a/app/views/admin/announcement/index.pug +++ b/app/views/admin/announcement/index.pug @@ -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 diff --git a/app/views/admin/components/file-upload-image.pug b/app/views/admin/components/file-upload-image.pug new file mode 100644 index 0000000..3ca78f6 --- /dev/null +++ b/app/views/admin/components/file-upload-image.pug @@ -0,0 +1,51 @@ +mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaultImage, currentImage, cropperOptions) + div(id= containerId).dtp-file-upload + form(method="POST", action= actionUrl, enctype="multipart/form-data", onsubmit= "return dtp.app.submitImageForm(event);").uk-form + .uk-margin + .uk-card.uk-card-default.uk-card-small + .uk-card-body + div(uk-grid).uk-flex-middle.uk-flex-center + div(class="uk-width-1-1 uk-width-auto@m") + .upload-image-container.size-512 + if !!currentImage + img(id= imageId, src= currentImage.path, class= imageClass).sb-large + else + img(id= imageId, src= defaultImage, class= imageClass) + + div(class="uk-width-1-1 uk-width-auto@m") + .uk-text-small.uk-margin + #file-select + .uk-margin(class="uk-text-center uk-text-left@m") + span.uk-text-middle Select an image + div(uk-form-custom).uk-margin-small-left + input( + type="file", + formenctype="multipart/form-data", + accept=".jpg,.png,image/jpeg,image/png", + data-file-select-container= containerId, + data-file-select="test-image-upload", + data-file-size-element= "file-size", + data-file-max-size= 15 * 1024000, + data-image-id= imageId, + data-cropper-options= cropperOptions, + onchange="return dtp.app.selectImageFile(event);", + ) + button(type="button", tabindex="-1").uk-button.uk-button-default Select + + #file-info(class="uk-text-center uk-text-left@m", hidden) + #file-name.uk-text-bold + if currentImage + div resolution: #[span#image-resolution-w= numeral(currentImage.metadata.width).format('0,0')]x#[span#image-resolution-h= numeral(currentImage.metadata.height).format('0,0')] + div size: #[span#file-size= numeral(currentImage.metadata.size).format('0,0.00b')] + div last modified: #[span#file-modified= moment(currentImage.created).format('MMM DD, YYYY')] + else + div resolution: #[span#image-resolution-w 512]x#[span#image-resolution-h 512] + div size: #[span#file-size N/A] + div last modified: #[span#file-modified N/A] + + .uk-card-footer + div(class="uk-flex-center", uk-grid) + #file-save-btn(hidden).uk-width-auto + button( + type="submit", + ).uk-button.uk-button-primary Save diff --git a/app/views/admin/components/menu.pug b/app/views/admin/components/menu.pug index 54df834..9573dbb 100644 --- a/app/views/admin/components/menu.pug +++ b/app/views/admin/components/menu.pug @@ -12,6 +12,18 @@ ul(uk-nav).uk-nav-default span.nav-item-icon i.fas.fa-cog span.uk-margin-small-left Settings + + li(class={ 'uk-active': (adminView === 'image-settings') }) + a(href="/admin/settings/images") + span.nav-item-icon + i.fas.fa-image + span.uk-margin-small-left Image Settings + + li(class={ 'uk-active': (adminView === 'otp') }) + a(href="/admin/otp") + span.nav-item-icon + i.fas.fa-cog + span.uk-margin-small-left Otp Settings li.uk-nav-divider diff --git a/app/views/admin/core-node/components/list-item.pug b/app/views/admin/core-node/components/list-item.pug index 382e2c2..cf38271 100644 --- a/app/views/admin/core-node/components/list-item.pug +++ b/app/views/admin/core-node/components/list-item.pug @@ -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 diff --git a/app/views/admin/core-node/view.pug b/app/views/admin/core-node/view.pug index 414e279..a728eaa 100644 --- a/app/views/admin/core-node/view.pug +++ b/app/views/admin/core-node/view.pug @@ -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 diff --git a/app/views/admin/newsletter/index.pug b/app/views/admin/newsletter/index.pug index f3874df..fa0f0a2 100644 --- a/app/views/admin/newsletter/index.pug +++ b/app/views/admin/newsletter/index.pug @@ -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. \ No newline at end of file diff --git a/app/views/admin/newsletter/job-status.pug b/app/views/admin/newsletter/job-status.pug index 7d7ac4e..6ea723c 100644 --- a/app/views/admin/newsletter/job-status.pug +++ b/app/views/admin/newsletter/job-status.pug @@ -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) \ No newline at end of file + 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) \ No newline at end of file diff --git a/app/views/admin/newsroom/editor.pug b/app/views/admin/newsroom/editor.pug index 684077b..55c5eb2 100644 --- a/app/views/admin/newsroom/editor.pug +++ b/app/views/admin/newsroom/editor.pug @@ -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' \ No newline at end of file + button(type="submit").uk-button.uk-button-primary.uk-border-rounded= feed ? 'Update Feed' : 'Add Feed' \ No newline at end of file diff --git a/app/views/admin/otp/index.pug b/app/views/admin/otp/index.pug new file mode 100644 index 0000000..bb8f872 --- /dev/null +++ b/app/views/admin/otp/index.pug @@ -0,0 +1,28 @@ +extends ../layouts/main +block content + + + div(uk-grid).uk-flex-middle + .uk-width-expand + h1.margin-remove Tokens + section.uk-section.uk-section-default.uk-section-xsmall + .uk-container + .uk-text-small + h4 This is where you will regenerate OTP tokens for your admin account and destroy your old OTP account. + //- .uk-width-auto + button( + type="button", + data-user= user._id, + onclick="return dtp.adminApp.generateOTPTokens(event);", + ).uk-button.dtp-button-danger + +renderButtonIcon('fa-repeat', 'Generate OTP Tokens') + //- regenerate route should set this so tokens can be viewed once. + if otpRegen + section.uk-section.uk-section-default.uk-section-xsmall + .uk-container + h3 You should save these tokens in a safe place. This is the only time you will see them. + p These tokens should be saved in a safe place so you can get into your account should you lose your 2FA device + each token of tokens + ul.uk-list.uk-list-divider + li + .uk-text-small= token.token diff --git a/app/views/admin/post/index.pug b/app/views/admin/post/index.pug index e4a1800..33b5ff5 100644 --- a/app/views/admin/post/index.pug +++ b/app/views/admin/post/index.pug @@ -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}` diff --git a/app/views/admin/settings/editor.pug b/app/views/admin/settings/editor.pug index 725c770..ec8aab5 100644 --- a/app/views/admin/settings/editor.pug +++ b/app/views/admin/settings/editor.pug @@ -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 \ No newline at end of file + button(type="submit").uk-button.dtp-button-primary Save Settings \ No newline at end of file diff --git a/app/views/admin/settings/images.pug b/app/views/admin/settings/images.pug new file mode 100644 index 0000000..9aa60be --- /dev/null +++ b/app/views/admin/settings/images.pug @@ -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 }, + ) \ No newline at end of file diff --git a/app/views/author/components/draft-list.pug b/app/views/author/components/draft-list.pug new file mode 100644 index 0000000..a1ea512 --- /dev/null +++ b/app/views/author/components/draft-list.pug @@ -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. \ No newline at end of file diff --git a/app/views/author/components/list-item.pug b/app/views/author/components/list-item.pug new file mode 100644 index 0000000..dcf11f5 --- /dev/null +++ b/app/views/author/components/list-item.pug @@ -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")} \ No newline at end of file diff --git a/app/views/author/components/list.pug b/app/views/author/components/list.pug new file mode 100644 index 0000000..16fcf87 --- /dev/null +++ b/app/views/author/components/list.pug @@ -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. \ No newline at end of file diff --git a/app/views/author/draft/index.pug b/app/views/author/draft/index.pug new file mode 100644 index 0000000..668d06a --- /dev/null +++ b/app/views/author/draft/index.pug @@ -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) + diff --git a/app/views/author/index.pug b/app/views/author/index.pug index 91cce08..1de8881 100644 --- a/app/views/author/index.pug +++ b/app/views/author/index.pug @@ -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 diff --git a/app/views/author/post/index.pug b/app/views/author/post/index.pug index 9c505a5..7ccc4b0 100644 --- a/app/views/author/post/index.pug +++ b/app/views/author/post/index.pug @@ -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) \ No newline at end of file + div(class="uk-width-1-1 uk-width-3-3@m") + +renderSectionTitle('Posts') + +renderPublishedPostList(published.posts) \ No newline at end of file diff --git a/app/views/components/library.pug b/app/views/components/library.pug index 9424f97..2c2b654 100644 --- a/app/views/components/library.pug +++ b/app/views/components/library.pug @@ -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 diff --git a/app/views/components/off-canvas.pug b/app/views/components/off-canvas.pug index 689900e..34e67ee 100644 --- a/app/views/components/off-canvas.pug +++ b/app/views/components/off-canvas.pug @@ -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 diff --git a/app/views/components/page-sidebar.pug b/app/views/components/page-sidebar.pug index 1a04b41..1e8c711 100644 --- a/app/views/components/page-sidebar.pug +++ b/app/views/components/page-sidebar.pug @@ -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 diff --git a/app/views/components/social-card/facebook.pug b/app/views/components/social-card/facebook.pug index d3a7432..a72fcf9 100644 --- a/app/views/components/social-card/facebook.pug +++ b/app/views/components/social-card/facebook.pug @@ -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) diff --git a/app/views/components/social-card/twitter.pug b/app/views/components/social-card/twitter.pug index f3aab08..6002338 100644 --- a/app/views/components/social-card/twitter.pug +++ b/app/views/components/social-card/twitter.pug @@ -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) diff --git a/app/views/layouts/main.pug b/app/views/layouts/main.pug index b344cac..3c5879e 100644 --- a/app/views/layouts/main.pug +++ b/app/views/layouts/main.pug @@ -118,4 +118,8 @@ html(lang='en') else script(src=`/dist/js/dtpsites-admin.min.js?v=${pkg.version}`, type="module") - block viewjs \ No newline at end of file + block viewjs + script. + window.addEventListener('dtp-load', function () { + return dtp.app.updatePostTimestamps(); + }); \ No newline at end of file diff --git a/app/views/newsroom/index.pug b/app/views/newsroom/index.pug index be94255..a6c0bdf 100644 --- a/app/views/newsroom/index.pug +++ b/app/views/newsroom/index.pug @@ -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 diff --git a/app/views/newsroom/unified-feed.pug b/app/views/newsroom/unified-feed.pug deleted file mode 100644 index 263f7b2..0000000 --- a/app/views/newsroom/unified-feed.pug +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/views/otp/new-account.pug b/app/views/otp/new-account.pug index 4f089c4..c50f6f9 100644 --- a/app/views/otp/new-account.pug +++ b/app/views/otp/new-account.pug @@ -5,7 +5,18 @@ block content .uk-container h1 2FA Setup Successful + section.uk-section.uk-section-default.uk-section-xsmall + .uk-container + h3 You should save these tokens in a safe place. This is the only time you will see them. + p These tokens should be saved in a safe place so you can get into your account should you lose your 2FA device + each token of otpAccount.backupTokens + ul.uk-list.uk-list-divider + li + .uk-text-small= token.token + + section.uk-section.uk-section-default.uk-section-xsmall .uk-container - p Your account is now enabled with access to #{site.name} #{otpServiceName}. - a(href= otpRedirectURL, title="Continue").uk-button.uk-button-primary.uk-border-pill Continue \ No newline at end of file + 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 + diff --git a/app/views/post/author/all.pug b/app/views/post/author/all.pug new file mode 100644 index 0000000..15be7d8 --- /dev/null +++ b/app/views/post/author/all.pug @@ -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 ) \ No newline at end of file diff --git a/app/views/post/author/components/credit.pug b/app/views/post/author/components/credit.pug new file mode 100644 index 0000000..a5bfc90 --- /dev/null +++ b/app/views/post/author/components/credit.pug @@ -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 \ No newline at end of file diff --git a/app/views/post/author/components/list.pug b/app/views/post/author/components/list.pug new file mode 100644 index 0000000..cc142b7 --- /dev/null +++ b/app/views/post/author/components/list.pug @@ -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 \ No newline at end of file diff --git a/app/views/post/author/view.pug b/app/views/post/author/view.pug new file mode 100644 index 0000000..bf431d5 --- /dev/null +++ b/app/views/post/author/view.pug @@ -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 ) \ No newline at end of file diff --git a/app/views/post/components/author-credit.pug b/app/views/post/components/author-credit.pug index 1f2d600..fe2dda5 100644 --- a/app/views/post/components/author-credit.pug +++ b/app/views/post/components/author-credit.pug @@ -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 \ No newline at end of file diff --git a/app/views/post/components/list-item.pug b/app/views/post/components/list-item.pug index dcf11f5..b51021b 100644 --- a/app/views/post/components/list-item.pug +++ b/app/views/post/components/list-item.pug @@ -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")} \ No newline at end of file + span published: + span(data-dtp-timestamp= post.created) \ No newline at end of file diff --git a/app/views/post/components/list.pug b/app/views/post/components/list.pug index 7deab63..9097030 100644 --- a/app/views/post/components/list.pug +++ b/app/views/post/components/list.pug @@ -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. \ No newline at end of file + div You have no authored posts. \ No newline at end of file diff --git a/app/views/post/editor.pug b/app/views/post/editor.pug index c37f5a5..56b97e3 100644 --- a/app/views/post/editor.pug +++ b/app/views/post/editor.pug @@ -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 diff --git a/app/views/post/view.pug b/app/views/post/view.pug index 6ada831..f8c7b3f 100644 --- a/app/views/post/view.pug +++ b/app/views/post/view.pug @@ -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 diff --git a/app/views/tag/components/list.pug b/app/views/tag/components/list.pug new file mode 100644 index 0000000..534d976 --- /dev/null +++ b/app/views/tag/components/list.pug @@ -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 \ No newline at end of file diff --git a/app/views/tag/view.pug b/app/views/tag/view.pug new file mode 100644 index 0000000..9ace8d5 --- /dev/null +++ b/app/views/tag/view.pug @@ -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}.` \ No newline at end of file diff --git a/app/views/welcome/core-home.pug b/app/views/welcome/core-home.pug index bb313d9..7298915 100644 --- a/app/views/welcome/core-home.pug +++ b/app/views/welcome/core-home.pug @@ -3,26 +3,34 @@ block content section.uk-section.uk-section-default .uk-container - - .uk-card.uk-card-default - .uk-card-header - h1.uk-card-title Select Community - - .uk-card-body - div(uk-grid).uk-grid-small - each core in connectedCores - div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl") - //- pre= JSON.stringify(connectedCores, null, 2) - a(href=`/auth/core/${core._id}`).uk-display-block.uk-link-reset - .dtp-core-list-item.uk-border-rounded - div(uk-grid).uk-grid-small.uk-flex-middle - .uk-width-auto - img(src=`http://${core.meta.domain}/img/icon/dtp-core.svg`, style="width: 48px; height: auto;") - .uk-width-expand - .core-name= core.meta.name - .core-description= core.meta.description - + + if Array.isArray(hosts) && (hosts.length > 0) + .uk-card.uk-card-default + .uk-card-header + h1.uk-card-title Select Community + .uk-card-body + div(uk-grid).uk-grid-small + each core in connectedCores + div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl") + //- pre= JSON.stringify(connectedCores, null, 2) + a(href=`/auth/core/${core._id}`).uk-display-block.uk-link-reset + .dtp-core-list-item.uk-border-rounded + div(uk-grid).uk-grid-small.uk-flex-middle + .uk-width-auto + img(src=`http://${core.meta.domain}/img/icon/dtp-core.svg`, style="width: 48px; height: auto;") + .uk-width-expand + .core-name= core.meta.name + .core-description= core.meta.description + .uk-card-footer div(uk-grid).uk-grid-small .uk-width-expand - +renderBackButton() \ No newline at end of file + +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() \ No newline at end of file diff --git a/app/views/welcome/index.pug b/app/views/welcome/index.pug index 0db601a..5dc3183 100644 --- a/app/views/welcome/index.pug +++ b/app/views/welcome/index.pug @@ -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 \ No newline at end of file diff --git a/app/views/welcome/login.pug b/app/views/welcome/login.pug index 49b4cfa..40b2a25 100644 --- a/app/views/welcome/login.pug +++ b/app/views/welcome/login.pug @@ -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 \ No newline at end of file + button(type="submit").uk-button.dtp-button-primary Login \ No newline at end of file diff --git a/app/views/welcome/signup.pug b/app/views/welcome/signup.pug index 7d8fb05..5af1a30 100644 --- a/app/views/welcome/signup.pug +++ b/app/views/welcome/signup.pug @@ -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 diff --git a/app/workers/media/job/sticker-ingest.js b/app/workers/media/job/sticker-ingest.js index 599f8c1..c4f6fcb 100644 --- a/app/workers/media/job/sticker-ingest.js +++ b/app/workers/media/job/sticker-ingest.js @@ -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, diff --git a/client/img/icon/blog.cybershell.xyz/icon-114x114.png b/client/img/icon/blog.cybershell.xyz/icon-114x114.png new file mode 100644 index 0000000..ec4a8bf Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-114x114.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-120x120.png b/client/img/icon/blog.cybershell.xyz/icon-120x120.png new file mode 100644 index 0000000..6ced2c5 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-120x120.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-144x144.png b/client/img/icon/blog.cybershell.xyz/icon-144x144.png new file mode 100644 index 0000000..69fba8d Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-144x144.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-150x150.png b/client/img/icon/blog.cybershell.xyz/icon-150x150.png new file mode 100644 index 0000000..84c9cf9 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-150x150.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-152x152.png b/client/img/icon/blog.cybershell.xyz/icon-152x152.png new file mode 100644 index 0000000..6331e97 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-152x152.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-16x16.png b/client/img/icon/blog.cybershell.xyz/icon-16x16.png new file mode 100644 index 0000000..9b9eaa4 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-16x16.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-180x180.png b/client/img/icon/blog.cybershell.xyz/icon-180x180.png new file mode 100644 index 0000000..edb005e Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-180x180.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-192x192.png b/client/img/icon/blog.cybershell.xyz/icon-192x192.png new file mode 100644 index 0000000..ecfbf39 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-192x192.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-256x256.png b/client/img/icon/blog.cybershell.xyz/icon-256x256.png new file mode 100644 index 0000000..486ca54 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-256x256.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-310x310.png b/client/img/icon/blog.cybershell.xyz/icon-310x310.png new file mode 100644 index 0000000..6211999 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-310x310.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-32x32.png b/client/img/icon/blog.cybershell.xyz/icon-32x32.png new file mode 100644 index 0000000..a9404e7 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-32x32.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-36x36.png b/client/img/icon/blog.cybershell.xyz/icon-36x36.png new file mode 100644 index 0000000..632a538 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-36x36.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-384x384.png b/client/img/icon/blog.cybershell.xyz/icon-384x384.png new file mode 100644 index 0000000..fc16bd6 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-384x384.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-48x48.png b/client/img/icon/blog.cybershell.xyz/icon-48x48.png new file mode 100644 index 0000000..ab5b186 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-48x48.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-512x512.png b/client/img/icon/blog.cybershell.xyz/icon-512x512.png new file mode 100644 index 0000000..02029bd Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-512x512.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-57x57.png b/client/img/icon/blog.cybershell.xyz/icon-57x57.png new file mode 100644 index 0000000..27fcadc Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-57x57.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-60x60.png b/client/img/icon/blog.cybershell.xyz/icon-60x60.png new file mode 100644 index 0000000..d0cd268 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-60x60.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-70x70.png b/client/img/icon/blog.cybershell.xyz/icon-70x70.png new file mode 100644 index 0000000..589372c Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-70x70.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-72x72.png b/client/img/icon/blog.cybershell.xyz/icon-72x72.png new file mode 100644 index 0000000..e4465cb Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-72x72.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-76x76.png b/client/img/icon/blog.cybershell.xyz/icon-76x76.png new file mode 100644 index 0000000..7c2c523 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-76x76.png differ diff --git a/client/img/icon/blog.cybershell.xyz/icon-96x96.png b/client/img/icon/blog.cybershell.xyz/icon-96x96.png new file mode 100644 index 0000000..149b5d7 Binary files /dev/null and b/client/img/icon/blog.cybershell.xyz/icon-96x96.png differ diff --git a/client/img/social-cards/blog.cybershell.xyz.png b/client/img/social-cards/blog.cybershell.xyz.png new file mode 100644 index 0000000..18eace6 Binary files /dev/null and b/client/img/social-cards/blog.cybershell.xyz.png differ diff --git a/client/js/index.js b/client/js/index.js index 3288abe..cb321bc 100644 --- a/client/js/index.js +++ b/client/js/index.js @@ -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 }); diff --git a/client/js/site-admin-app.js b/client/js/site-admin-app.js index ff6bb1f..1262d91 100644 --- a/client/js/site-admin-app.js +++ b/client/js/site-admin-app.js @@ -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; } } diff --git a/client/js/site-app.js b/client/js/site-app.js index 247cbf0..2f2289a 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -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; \ No newline at end of file diff --git a/client/less/site/button.less b/client/less/site/button.less index f63fcaf..27bbe18 100644 --- a/client/less/site/button.less +++ b/client/less/site/button.less @@ -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; } } diff --git a/config/limiter.js b/config/limiter.js index 1c1b788..a7c417d 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -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, diff --git a/deploy-cybershell b/deploy-cybershell new file mode 100755 index 0000000..4c02c22 --- /dev/null +++ b/deploy-cybershell @@ -0,0 +1,7 @@ +#!/bin/sh + +git pull prod master +yarn --production=false +gulp build + +./restart-production \ No newline at end of file diff --git a/dtp-media-engine.js b/dtp-media-engine.js deleted file mode 100644 index 8e891ba..0000000 --- a/dtp-media-engine.js +++ /dev/null @@ -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); - } - -})(); \ No newline at end of file diff --git a/iconImport.js b/iconImport.js new file mode 100644 index 0000000..25bdb14 --- /dev/null +++ b/iconImport.js @@ -0,0 +1,26 @@ +(async() => { + const siteDomain = "blog.cybershell.xyz"; + const sharp = require('sharp'); + const fs = require('fs'); + const path = require('path'); + // const iconDir = path.join(__dirname, siteDomain); + const siteImagesDir = path.join(__dirname, 'client', 'img'); + const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain) + const sourceIconFile = 'thumbnail.png'; + const sourceIconFilePath = path.join(__dirname, sourceIconFile); + const sizes = [16, 32, 36, 48, 57, 60, 70, 72, 76, 96, 114, 120, 144, 150, 152, 180, 192, 256, 310, 384, 512]; + + await fs.promises.mkdir(siteIconDir, { force: true, recursive: true }); + for (var size of sizes) { + await sharp(sourceIconFilePath).resize({ + fit: sharp.fit.contain, + width: size, + height: size + }).png() + .toFile(path.join(siteIconDir, `icon-${size}x${size}.png`)); + } + + // await fs.promises.cp(sourceIconFilePath, path.join(siteIconDir, `${siteDomain}.png`)); + await fs.promises.cp(sourceIconFilePath, path.join(siteImagesDir, 'social-cards', `${siteDomain}.png`)); + // await fs.promises.cp(iconDir, path.join(siteImagesDir, 'icon' ), { recursive: true }); +})() \ No newline at end of file diff --git a/lib/site-common.js b/lib/site-common.js index 3732ce3..2b62eb3 100644 --- a/lib/site-common.js +++ b/lib/site-common.js @@ -12,29 +12,11 @@ const striptags = require('striptags'); const { SiteLog } = require(path.join(__dirname, 'site-log')); const { SiteAsync } = require(path.join(__dirname, 'site-async')); -const EventEmitter2 = require('eventemitter2'); -class SiteCommon extends EventEmitter2 { - - constructor (dtp, component, options) { - // ensure valid options - options = Object.assign({ }, options); - - // provide DTP's default EventEmitter2 configuration - options.emitter = Object.assign({ - wildcard: false, - delimiter: '.', - newListener: false, - removeListener: false, - maxListeners: 64, - verboseMemoryLeak: process.env.NODE_ENV === 'local', - ignoreErrors: false, - }, options); - - // construct the EventEmitter2 instance via super() - super(options.emitter); +const Events = require('events'); +class SiteCommon extends Events { - // store options *after* super() because you can't alter `this` prior - this.options = options; + constructor (dtp, component) { + super(); this.dtp = dtp; this.component = component; @@ -61,14 +43,6 @@ class SiteCommon extends EventEmitter2 { }, 1); } - getEventName (name) { - return `dtp.${this.component.slug}.${name}`; - } - - async emitDtpEvent (name, params) { - await this.emitAsync(this.getEventName(name), params); - } - async getJobQueue (name) { if (this.jobQueues[name]) { return this.jobQueues[name]; diff --git a/lib/site-controller.js b/lib/site-controller.js index 217792f..2bf96a0 100644 --- a/lib/site-controller.js +++ b/lib/site-controller.js @@ -8,6 +8,7 @@ const path = require('path'); const multer = require('multer'); const { SiteCommon } = require(path.join(__dirname, 'site-common')); +const { SiteError } = require(path.join(__dirname, 'site-error')); class SiteController extends SiteCommon { @@ -59,6 +60,10 @@ class SiteController extends SiteCommon { const { csrfToken } = this.dtp.platform.services; return csrfToken.create(req, { name }); } + + async badRoute (res, req, next) { + return next(new SiteError(400, "Page does not exist.")); + } } module.exports.SiteController = SiteController; diff --git a/lib/site-platform.js b/lib/site-platform.js index 3b390d0..8dd700e 100644 --- a/lib/site-platform.js +++ b/lib/site-platform.js @@ -34,12 +34,22 @@ module.connectDatabase = async (/*dtp*/) => { host: process.env.MONGODB_HOST, database: process.env.MONGODB_DATABASE, }); - const mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`; + const mongoConnectionInfo = { + host: process.env.MONGODB_HOST, + db: process.env.MONGODB_DATABASE, + username: encodeURIComponent(process.env.MONGODB_USERNAME), + password: encodeURIComponent(process.env.MONGODB_PASSWORD), + options: process.env.MONGODB_OPTIONS || '', + }; + let mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`; + if (process.env.NODE_ENV === 'production'){ + mongoConnectUri = `mongodb://${mongoConnectionInfo.username}:${mongoConnectionInfo.password}@${mongoConnectionInfo.host}/${mongoConnectionInfo.options}`; + } module.db = await mongoose.connect(mongoConnectUri, { socketTimeoutMS: 0, keepAlive: true, keepAliveInitialDelay: 300000, - dbName: process.env.MONGODB_DATABASE, + dbName: mongoConnectionInfo.db, }); module.log.info('connected to MongoDB'); } catch (error) { @@ -184,12 +194,6 @@ module.exports.startPlatform = async (dtp) => { try { module.log = new SiteLog(module, dtp.config.component); - - if (process.env.NODE_ENV === 'local') { - module.log.alert('allowing self-signed certificates for host-to-host communications', { env: process.env.NODE_ENV }); - process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - } - dtp.config.jobQueues = require(path.join(dtp.config.root, 'config', 'job-queues')); await module.connectDatabase(dtp); @@ -257,16 +261,11 @@ module.exports.startWebServer = async (dtp) => { return next(); } - function serviceWorkerAllowed (req, res, next) { - res.set('Service-Worker-Allowed', '/'); - return next(); - } - /* * Static file services (project) */ module.app.use(express.static(path.join(dtp.config.root, 'client'))); - module.app.use('/dist', cacheOneDay, serviceWorkerAllowed, express.static(path.join(dtp.config.root, 'dist'))); + module.app.use('/dist', cacheOneDay, express.static(path.join(dtp.config.root, 'dist'))); module.app.use('/img', cacheOneDay, express.static(path.join(dtp.config.root, 'client', 'img'))); /* diff --git a/package.json b/package.json index be46700..008544f 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,10 @@ "diskusage-ng": "^1.0.2", "disposable-email-provider-domains": "^1.0.9", "dotenv": "^16.0.0", - "dtp-jshint-reporter": "git+https://git.digitaltelepresence.com/digital-telepresence/dtp-jshint-reporter.git#master", + "dtp-jshint-reporter": "ssh://git@git.digitaltelepresence.com:digital-telepresence/dtp-jshint-reporter.git#master", "ein-validator": "^1.0.1", "email-domain-check": "^1.1.4", "email-validator": "^2.0.4", - "eventemitter2": "^6.4.9", "execa": "^6.1.0", "express": "^4.17.3", "express-limiter": "^1.6.1", @@ -49,7 +48,6 @@ "libphonenumber-js": "^1.9.49", "link-preview-js": "^3.0.4", "marked": "^4.0.12", - "mediasoup": "3", "method-override": "^3.0.0", "mime": "^3.0.0", "minio": "^7.0.26", diff --git a/restart-production b/restart-production old mode 100644 new mode 100755 diff --git a/start-production b/start-production index e63207b..6472486 100755 --- a/start-production +++ b/start-production @@ -1,6 +1,6 @@ #!/bin/bash -sudo supervisorctl start \ +sudo supervisord ctl start \ sites-host-services:* \ sites-reeeper:* \ sites-newsletter:* \ diff --git a/stop-production b/stop-production index f798ab6..857dd8d 100755 --- a/stop-production +++ b/stop-production @@ -1,6 +1,6 @@ #!/bin/bash -sudo supervisorctl stop \ +sudo supervisord ctl stop \ sites:* \ sites-chat:* \ sites-media:* \ diff --git a/supervisord/dtp-base-newsroom.conf b/supervisord/dtp-base-newsroom.conf deleted file mode 100644 index 50b4b8c..0000000 --- a/supervisord/dtp-base-newsroom.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:dtp-base:newsroom] -numprocs=1 -process_name=%(program_name)s_%(process_num)02d -command=/home/dtp/.nvm/versions/node/v16.13.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsroom.js -directory=/home/dtp/live/dtp-base -autostart=true -autorestart=true -startretries=3 -stopsignal=INT -stderr_logfile=/var/log/dtp-base/newsroom.err.log -stdout_logfile=/var/log/dtp-base/newsroom.out.log -user=dtp -environment=HOME='/home/dtp/live/dtp-base',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=newsroom \ No newline at end of file diff --git a/supervisord/dtp-sites-chat.conf b/supervisord/dtp-sites-chat.conf new file mode 100644 index 0000000..748fec9 --- /dev/null +++ b/supervisord/dtp-sites-chat.conf @@ -0,0 +1,16 @@ +[group:sites-chat] +programs=dtp-sites-chat + +[program:dtp-sites-chat] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/chat.js +directory=/home/cybershell/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stderr_logfile=/var/log/dtp-sites/host-services.err.log +stdout_logfile=/var/log/dtp-sites/host-services.out.log +user=cybershell +environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-chat \ No newline at end of file diff --git a/supervisord/dtp-sites-groups.conf b/supervisord/dtp-sites-groups.conf new file mode 100644 index 0000000..43232f1 --- /dev/null +++ b/supervisord/dtp-sites-groups.conf @@ -0,0 +1,8 @@ + + +[group:sites-media] +program=dtp-sites-media +[group:sites-chat] +program=dtp-sites-chat +[group:sites] +program=dtp-sites \ No newline at end of file diff --git a/supervisord/dtp-sites-host-services.conf b/supervisord/dtp-sites-host-services.conf index ef11c57..3dde470 100644 --- a/supervisord/dtp-sites-host-services.conf +++ b/supervisord/dtp-sites-host-services.conf @@ -1,13 +1,17 @@ +[group:sites-host-services] +programs=host-services + + [program:host-services] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/dtp/.nvm/versions/node/v16.13.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/host-services.js -directory=/home/dtp/live/dtp-sites +command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/host-services.js +directory=/home/cybershell/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT stderr_logfile=/var/log/dtp-sites/host-services.err.log stdout_logfile=/var/log/dtp-sites/host-services.out.log -user=dtp -environment=HOME='/home/dtp/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=host-services \ No newline at end of file +user=cybershell +environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=host-services \ No newline at end of file diff --git a/supervisord/dtp-sites-media.conf b/supervisord/dtp-sites-media.conf new file mode 100644 index 0000000..38852f0 --- /dev/null +++ b/supervisord/dtp-sites-media.conf @@ -0,0 +1,17 @@ + +[group:sites-media] +programs=dtp-sites-media + +[program:dtp-sites-media] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/media.js +directory=/home/cybershell/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stderr_logfile=/var/log/dtp-sites/host-services.err.log +stdout_logfile=/var/log/dtp-sites/host-services.out.log +user=cybershell +environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-media \ No newline at end of file diff --git a/supervisord/dtp-sites-newsletter.conf b/supervisord/dtp-sites-newsletter.conf index dbe583b..b04f625 100644 --- a/supervisord/dtp-sites-newsletter.conf +++ b/supervisord/dtp-sites-newsletter.conf @@ -1,13 +1,16 @@ -[program:dtp-sites:newsletter] +[group:sites-newsletter] +programs=dtp-sites-newsletter + +[program:dtp-sites-newsletter] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/dtp/.nvm/versions/node/v16.13.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsletter.js -directory=/home/dtp/live/dtp-sites +command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsletter.js +directory=/home/cybershell/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT stderr_logfile=/var/log/dtp-sites/newsletter.err.log stdout_logfile=/var/log/dtp-sites/newsletter.out.log -user=dtp -environment=HOME='/home/dtp/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-newsletter \ No newline at end of file +user=cybershell +environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-newsletter \ No newline at end of file diff --git a/supervisord/dtp-sites-newsroom.conf b/supervisord/dtp-sites-newsroom.conf new file mode 100644 index 0000000..53159dc --- /dev/null +++ b/supervisord/dtp-sites-newsroom.conf @@ -0,0 +1,16 @@ +[group:sites-newsroom] +programs=dtp-sites-newsroom + +[program:dtp-sites-newsroom] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/newsroom.js +directory=/home/cybershell/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stderr_logfile=/var/log/dtp-sites/newsroom.err.log +stdout_logfile=/var/log/dtp-sites/newsroom.out.log +user=cybershell +environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-newsroom \ No newline at end of file diff --git a/supervisord/dtp-sites-reeper.conf b/supervisord/dtp-sites-reeper.conf new file mode 100644 index 0000000..23c9a94 --- /dev/null +++ b/supervisord/dtp-sites-reeper.conf @@ -0,0 +1,16 @@ +[group:sites-reeeper] +programs=reeeper + +[program:reeeper] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/reeeper.js +directory=/home/cybershell/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stderr_logfile=/var/log/dtp-sites/host-services.err.log +stdout_logfile=/var/log/dtp-sites/host-services.out.log +user=cybershell +environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=sites-reeeper \ No newline at end of file diff --git a/supervisord/dtp-sites.conf b/supervisord/dtp-sites.conf index 2df7a0b..dc8e153 100644 --- a/supervisord/dtp-sites.conf +++ b/supervisord/dtp-sites.conf @@ -1,13 +1,16 @@ +[group:sites] +programs=dtp-sites + [program:dtp-sites] numprocs=1 process_name=%(program_name)s_%(process_num)02d -command=/home/dtp/.nvm/versions/node/v16.13.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 dtp-sites.js -directory=/home/dtp/live/dtp-sites +command=/home/cybershell/.nvm/versions/node/v18.12.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 dtp-sites.js +directory=/home/cybershell/live/dtp-sites autostart=true autorestart=true startretries=3 stopsignal=INT stderr_logfile=/var/log/dtp-sites/dtp-sites.err.log stdout_logfile=/var/log/dtp-sites/dtp-sites.out.log -user=dtp -environment=HOME='/home/dtp/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=dtp-sites \ No newline at end of file +user=cybershell +environment=HOME='/home/cybershell/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=dtp-sites \ No newline at end of file diff --git a/thumbnail.png b/thumbnail.png new file mode 100644 index 0000000..18eace6 Binary files /dev/null and b/thumbnail.png differ diff --git a/update-master b/update-master new file mode 100755 index 0000000..cfce2a5 --- /dev/null +++ b/update-master @@ -0,0 +1,3 @@ +#!/bin/bash +git fetch origin master +git merge -s ours origin/master \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e2c8439..6778cc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1063,11 +1063,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234" integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw== -"@types/node@^16.11.10": - version "16.18.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" - integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== - "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -3249,9 +3244,9 @@ drange@^1.0.2: resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8" integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA== -"dtp-jshint-reporter@git+https://git.digitaltelepresence.com/digital-telepresence/dtp-jshint-reporter.git#master": - version "1.0.4" - resolved "git+https://git.digitaltelepresence.com/digital-telepresence/dtp-jshint-reporter.git#517c2f8055140b89cd3bbfff1cdf33669b416322" +"dtp-jshint-reporter@ssh://git@git.digitaltelepresence.com:digital-telepresence/dtp-jshint-reporter.git#master": + version "1.0.2" + resolved "ssh://git@git.digitaltelepresence.com:digital-telepresence/dtp-jshint-reporter.git#68b078b75cd6d048a9bf9bdc9b30ccc2a2145c4f" dependencies: chalk "^4.1.1" @@ -3633,16 +3628,6 @@ etag@1.8.1, etag@^1.8.1, etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter2@^6.4.9: - version "6.4.9" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" - integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== - eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -5880,17 +5865,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mediasoup@3: - version "3.10.12" - resolved "https://registry.yarnpkg.com/mediasoup/-/mediasoup-3.10.12.tgz#509c8c8ebe950dbb056ed8dbd077b3a5e902a229" - integrity sha512-cb+Jn51QQOUrZONT1vUzoIIY0wVGsYVNm8ghOGLcmpM90IVceAhBtqXsT/zUgSSMIS/ZUkUOo8YnyTBZMeqkJg== - dependencies: - "@types/node" "^16.11.10" - debug "^4.3.4" - h264-profile-level-id "^1.0.1" - supports-color "^9.2.3" - uuid "^9.0.0" - memory-fs@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" @@ -8527,11 +8501,6 @@ supports-color@^8.0.0, supports-color@^8.1.1: dependencies: has-flag "^4.0.0" -supports-color@^9.2.3: - version "9.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.3.tgz#a6e2c97fc20c80abecd69e50aebe4783ff77d45a" - integrity sha512-aszYUX/DVK/ed5rFLb/dDinVJrQjG/vmU433wtqVSD800rYsJNWxh2R3USV90aLSU+UsyQkbNeffVLzc6B6foA== - sver-compat@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" @@ -9162,11 +9131,6 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== - v8flags@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656"