admin/management

master
rob 1 year ago
parent 85b1f0757e
commit 3b8a832cad

@ -43,10 +43,12 @@ class AdminController extends SiteController {
);
router.use('/announcement', await this.loadChild(path.join(__dirname, 'admin', 'announcement')));
router.use('/attachment', await this.loadChild(path.join(__dirname, 'admin', 'attachment')));
router.use('/content-report', await this.loadChild(path.join(__dirname, 'admin', 'content-report')));
router.use('/core-node', await this.loadChild(path.join(__dirname, 'admin', 'core-node')));
router.use('/core-user', await this.loadChild(path.join(__dirname, 'admin', 'core-user')));
router.use('/host', await this.loadChild(path.join(__dirname, 'admin', 'host')));
router.use('/image', await this.loadChild(path.join(__dirname, 'admin', 'image')));
router.use('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));
router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));

@ -0,0 +1,138 @@
// admin/attachment.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const { SiteController } = require('../../../lib/site-lib');
class AttachmentAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'attachment';
return next();
});
router.param('attachmentId', this.populateAttachmentId.bind(this));
router.post('/:attachmentId', this.postUpdateAttachment.bind(this));
router.get('/create', this.getAttachmentEditor.bind(this));
router.get('/:attachmentId', this.getAttachmentEditor.bind(this));
router.get('/', this.getDashboard.bind(this));
router.delete('/:attachmentId', this.deleteAttachment.bind(this));
return router;
}
async populateAttachmentId (req, res, next, attachmentId) {
const {
attachment: attachmentService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.attachment = await attachmentService.getById(attachmentId);
return next();
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateAttachmentId',
message: `failed to populate attachment: ${error.message}`,
data: { attachmentId, error },
});
return next(error);
}
}
async postUpdateAttachment (req, res, next) {
const {
attachment: attachmentService,
logan: loganService,
} = this.dtp.services;
try {
await attachmentService.update(res.locals.attachment, req.body);
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'postUpdateAttachment',
data: {
attachment: {
_id: res.locals.attachment._id,
},
},
});
res.redirect('/admin/attachment');
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postUpdateAttachment',
message: `failed to update attachment: ${error.message}`,
data: { error },
});
return next(error);
}
}
async getAttachmentEditor (req, res) {
res.render('admin/attachment/editor');
}
async getDashboard (req, res, next) {
const { attachment: attachmentService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.attachments = await attachmentService.getRecent(res.locals.pagination);
res.render('admin/attachment/index');
} catch (error) {
return next(error);
}
}
async deleteAttachment (req, res) {
const {
attachment: attachmentService,
logan: loganService,
} = this.dtp.services;
try {
const displayList = this.createDisplayList('delete-attachment');
await attachmentService.remove(res.locals.attachment);
displayList.reload();
res.status(200).json({ success: true, displayList });
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'deleteAttachment',
data: { attachment: { _id: res.locals.attachment._id } },
});
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'deleteAttachment',
message: `failed to delete attachment: ${error.message}`,
data: { error },
});
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {
name: 'adminAttachment',
slug: 'adminAttachment',
className: 'AttachmentAdminController',
create: async (dtp) => { return new AttachmentAdminController(dtp); },
};

@ -0,0 +1,107 @@
// admin/image.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const { SiteController } = require('../../../lib/site-lib');
class ImageAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'image';
return next();
});
router.param('imageId', this.populateImageId.bind(this));
router.get('/:imageId', this.getImageView.bind(this));
router.get('/', this.getDashboard.bind(this));
router.delete('/:imageId', this.deleteImage.bind(this));
return router;
}
async populateImageId (req, res, next, imageId) {
const {
image: imageService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.image = await imageService.getImageById(imageId);
return next();
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateImageId',
message: `failed to populate image: ${error.message}`,
data: { imageId, error },
});
return next(error);
}
}
async getImageView (req, res) {
res.render('admin/image/view');
}
async getDashboard (req, res, next) {
const { image: imageService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.images = await imageService.getRecentImages(res.locals.pagination);
res.render('admin/image/index');
} catch (error) {
return next(error);
}
}
async deleteImage (req, res) {
const {
image: imageService,
logan: loganService,
} = this.dtp.services;
try {
const displayList = this.createDisplayList('delete-image');
await imageService.deleteImage(res.locals.image);
displayList.reload();
res.status(200).json({ success: true, displayList });
loganService.sendRequestEvent(module.exports, req, {
level: 'info',
event: 'deleteImage',
data: { image: { _id: res.locals.image._id } },
});
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'deleteImage',
message: `failed to delete image: ${error.message}`,
data: { error },
});
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {
name: 'adminImage',
slug: 'adminImage',
className: 'ImageAdminController',
create: async (dtp) => { return new ImageAdminController(dtp); },
};

@ -9,7 +9,7 @@ const fs = require('fs');
const express = require('express');
const mongoose = require('mongoose');
const { SiteController/*, SiteError*/ } = require('../../lib/site-lib');
const { SiteController, SiteError } = require('../../lib/site-lib');
class ImageController extends SiteController {
@ -60,6 +60,9 @@ class ImageController extends SiteController {
try {
res.locals.imageId = mongoose.Types.ObjectId(imageId);
res.locals.image = await this.dtp.services.image.getImageById(res.locals.imageId);
if (!res.locals.image) {
throw new SiteError(404, 'Image not found');
}
return next();
} catch (error) {
this.log.error('failed to populate image', { error });

@ -119,13 +119,13 @@ class UserController extends SiteController {
);
router.get(
'/:userId/otp-setup',
'/:localUserId/otp-setup',
limiterService.createMiddleware(limiterService.config.user.getOtpSetup),
otpSetup,
this.getOtpSetup.bind(this),
);
router.get(
'/:userId/otp-disable',
'/:localUserId/otp-disable',
limiterService.createMiddleware(limiterService.config.user.getOtpDisable),
authRequired,
this.getOtpDisable.bind(this),
@ -148,7 +148,7 @@ class UserController extends SiteController {
);
router.delete(
'/:userId/profile-photo',
'/:localUserId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto),
authRequired,
checkProfileOwner,
@ -334,6 +334,12 @@ class UserController extends SiteController {
});
} catch (error) {
this.log.error('failed to create new user', { error });
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'postCreateUser',
message: `failed to create user account: ${error.message}`,
data: { definition: req.body, error },
});
return next(error);
}
}

@ -126,6 +126,23 @@ class AttachmentService extends SiteService {
return attachments;
}
/**
* Access all attachments sorted by most recent with pagination. This is for
* use by Admin tools.
* @param {*} pagination required pagination parameters (skip and cpp)
* @returns Array of attachments
*/
async getRecent (pagination) {
const attachments = await Attachment
.find()
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateAttachment)
.lean();
return attachments;
}
/**
*
* @param {mongoose.Types.ObjectId} attachmentId The ID of the attachment

@ -100,6 +100,18 @@ class ImageService extends SiteService {
return images;
}
async getRecentImages (pagination) {
const images = await SiteImage
.find()
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateImage)
.lean();
const totalImageCount = await SiteImage.estimatedDocumentCount();
return { images, totalImageCount };
}
async deleteImage(image) {
const { minio: minioService } = this.dtp.services;

@ -19,9 +19,6 @@ class LoganService extends SiteService {
}
async sendRequestEvent (component, req, event) {
if (process.env.DTP_LOGAN !== 'enabled') {
return;
}
if (req.user) {
event.data = event.data || { };
event.data.user = {
@ -34,20 +31,21 @@ class LoganService extends SiteService {
}
async sendEvent (component, event) {
if (process.env.DTP_LOGAN !== 'enabled') {
return;
}
try {
const loganScheme = process.env.DTP_LOGAN_SCHEME || 'http';
const loganUrl = `${loganScheme}://${process.env.DTP_LOGAN_HOST}/api/event`;
event.host = os.hostname();
event['component.slug'] = component.slug;
event['component.name'] = component.className || component.name;
this.log[event.level]('sending Logan event', { event });
this.log[event.level]('application event', { event });
if (process.env.DTP_LOGAN !== 'enabled') {
return;
}
const loganScheme = process.env.DTP_LOGAN_SCHEME || 'http';
const loganUrl = `${loganScheme}://${process.env.DTP_LOGAN_HOST}/api/event`;
const payload = JSON.stringify(event);
const response = await fetch(loganUrl, {
method: 'POST',
headers: {

@ -69,6 +69,7 @@ class UserService extends SiteService {
} = this.dtp.services;
try {
this.checkRestrictedKeys('create', userDefinition);
userDefinition.email = userDefinition.email.trim().toLowerCase();
// strip characters we don't want to allow in username
@ -205,6 +206,8 @@ class UserService extends SiteService {
throw SiteError(403, 'Invalid user account operation');
}
this.checkRestrictedKeys('create', userDefinition);
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -683,7 +686,18 @@ class UserService extends SiteService {
const { image: imageService } = this.dtp.services;
this.log.info('remove profile photo', { user: user._id });
user = await this.getUserAccount(user._id);
switch (user.type) {
case 'User':
user = await this.getLocalUserAccount(user._id);
break;
case 'CoreUser':
user = await this.getCoreUserAccount(user._id);
break;
default:
throw new SiteError(400, 'Invalid User type');
}
if (user.picture.large) {
await imageService.deleteImage(user.picture.large);
}
@ -834,6 +848,28 @@ class UserService extends SiteService {
await stickerService.removeForUser(user);
await userNotificationService.removeForUser(user);
}
checkRestrictedKeys (method, definition) {
const { logan: loganService } = this.dtp.services;
const restrictedKeys = [
'isAdmin', 'isModerator', 'isEmailVerified',
'canLogin', 'canChat', 'canComment', 'canReport',
'optInSystem', 'optInMarketing',
];
const keys = Object.keys(definition);
for (const restrictedKey of restrictedKeys) {
if (keys.includes(restrictedKey)) {
loganService.sendEvent(module.exports, {
level: 'alert',
event: method,
message: 'malicious fields detected',
data: { definition },
});
throw new SiteError(403, 'invalid request');
}
}
}
}
module.exports = {

@ -0,0 +1,21 @@
extends ../layouts/main
block content
h1 Attachments
if Array.isArray(attachments) && (attachments.length > 0)
ul.uk-list.uk-list-divider
each attachment in attachments
li
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-expand
//- had to abort while writing the renderer for an attachment.
//- will be back to finish this and have an attachment browser/manager.
pre= JSON.stringify(attachment, null, 2)
.uk-width-auto
button(type="button", data-attachment-id= attachment._id, onclick="return dtp.adminApp.deleteAttachment(event);").uk-button.dtp-button-danger.uk-border-rounded
span
i.fas.fa-trash
else
div There are no attachments.

@ -33,6 +33,18 @@ ul(uk-nav).uk-nav-default
i.fas.fa-bullhorn
span.uk-margin-small-left Announcements
li(class={ 'uk-active': (adminView === 'attachment') })
a(href="/admin/attachment")
span.nav-item-icon
i.fas.fa-file
span.uk-margin-small-left Attachments
li(class={ 'uk-active': (adminView === 'image') })
a(href="/admin/image")
span.nav-item-icon
i.fas.fa-image
span.uk-margin-small-left Images
li(class={ 'uk-active': (adminView === 'user') })
a(href="/admin/user")
span.nav-item-icon

@ -0,0 +1,45 @@
extends ../layouts/main
block content
include ../user/components/list-item
include ../../components/pagination-bar
h1.uk-text-center Image Manager
if Array.isArray(images.images) && (images.images.length > 0)
div(uk-grid).uk-flex-center
each image in images.images
.uk-width-medium
.uk-margin-small(uk-lightbox)
a(href=`/image/${image._id}`, data-type="image", data-caption=`id: ${image._id}`)
div
img(src= `/image/${image._id}`).responsive
if image.owner
.uk-margin-small
+renderUserListItem(image.owner)
.uk-margin-small.uk-text-center
button(type="button").uk-button.uk-button-default.uk-button-small.uk-border-rounded
span Image Menu
div(uk-drop={ mode: 'click', pos: 'top-center' }).uk-card.uk-card-default.uk-card-small.uk-border-rounded
.uk-card-header
.uk-text-small.uk-text-muted.uk-text-center id:#{image._id}
.uk-card-body
ul.uk-nav.uk-dropdown-nav
li
a(href="#", data-image-id= image._id, onclick="dtp.adminApp.deleteImage(event);")
span
i.fas.fa-trash
span.uk-margin-small-left Delete image
li
a(href=`/admin/image/${image._id}/archive-user`).uk-text-truncate
span
i.fas.fa-file-archive
span.uk-margin-small-left Archive and ban #[span.uk-text-bold= image.owner.username]
+renderPaginationBar('/admin/image', images.totalImageCount)
else
.uk-text-center There are no images.

@ -7,7 +7,8 @@ mixin renderUserListItem (user)
.uk-text-small.uk-text-muted
a(href= getUserProfileUrl(user))= user.username
.uk-text-small.uk-text-truncate= user.bio
.uk-text-small.uk-text-muted created #{moment(user.created).fromNow()}
if user.created
.uk-text-small.uk-text-muted created #{moment(user.created).fromNow()}
.uk-width-auto
a(href=`/admin/user/local/${user._id}`, uk-tooltip={ title: 'Manage user account' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded
span

@ -452,6 +452,13 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
return;
}
async deleteImage (event) {
const target = event.currentTarget || event.target;
const imageId = target.getAttribute('data-image-id');
const response = await fetch(`/admin/image/${imageId}`, { method: 'DELETE' });
return this.processResponse(response);
}
}
dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp;
Loading…
Cancel
Save