// admin/user.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const express = require('express'); const { SiteController, SiteError } = require('../../../lib/site-lib'); class UserAdminController extends SiteController { constructor (dtp) { super(dtp, module.exports); } async start ( ) { const { jobQueue: jobQueueService } = this.dtp.services; this.jobQueues = { }; this.jobQueues.reeeper = await jobQueueService.getJobQueue( 'reeeper', this.dtp.config.jobQueues.reeeper, ); const router = express.Router(); router.use(async (req, res, next) => { res.locals.currentView = 'admin'; res.locals.adminView = 'user'; return next(); }); router.param('localUserId', this.populateLocalUserId.bind(this)); router.param('archiveJobId', this.populateArchiveJobId.bind(this)); router.param('archiveId', this.populateArchiveId.bind(this)); router.post('/local/:localUserId/archive', this.postArchiveLocalUser.bind(this)); router.post('/local/:localUserId', this.postUpdateLocalUser.bind(this)); router.get('/local/:localUserId/archive/confirm', this.getArchiveLocalUserConfirm.bind(this)); router.get('/local/:localUserId', this.getLocalUserView.bind(this)); router.get('/archive/job/:archiveJobId', this.getUserArchiveJobView.bind(this)); router.post('/archive/:archiveId/action', this.postArchiveAction.bind(this)); router.get('/archive/:archiveId/file', this.getUserArchiveFile.bind(this)); router.get('/archive/:archiveId', this.getUserArchiveView.bind(this)); router.get('/archive', this.getUserArchiveIndex.bind(this)); router.get('/', this.getHomeView.bind(this)); return router; } async populateLocalUserId (req, res, next, localUserId) { const { user: userService } = this.dtp.services; try { res.locals.userAccount = await userService.getLocalUserAccount(localUserId); if (!res.locals.userAccount) { throw new SiteError(404, 'User not found'); } return next(); } catch (error) { return next(error); } } async populateArchiveJobId (req, res, next, archiveJobId) { try { res.locals.job = await this.jobQueues.reeeper.getJob(archiveJobId); if (!res.locals.job) { throw new SiteError(404, 'Job not found'); } return next(); } catch (error) { this.log.error('failed to populate Bull queue job', { archiveJobId, error }); return next(error); } } async populateArchiveId (req, res, next, archiveId) { const { user: userService } = this.dtp.services; try { res.locals.archive = await userService.getArchiveById(archiveId); if (!res.locals.archive) { throw new SiteError(404, 'Archive not found'); } return next(); } catch (error) { this.log.error('failed to populate UserArchive', { archiveId, error }); return next(error); } } async postArchiveLocalUser (req, res, next) { const { logan: loganService, user: userService, } = this.dtp.services; try { const user = await userService.getLocalUserAccount(req.body.userId); if (!user) { throw new SiteError(404, 'User not found'); } if (req.user && req.user._id.equals(user._id)) { throw new SiteError(400, "You can't archive yourself"); } res.locals.job = await userService.archiveLocalUser(user); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postArchiveUser', data: { job: res.locals.job.id, user: user, }, }); res.redirect(`/admin/user/archive/job/${res.locals.job.id}`); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postArchiveUser', data: { offender: { _id: req.body.userId, }, error, }, }); return next(error); } } async postUpdateLocalUser (req, res, next) { const { logan: loganService, user: userService, } = this.dtp.services; try { this.log.debug('local user update', { action: req.body.action }); switch (req.body.action) { case 'update': if (req.user._id.equals(res.locals.userAccount._id)) { if (req.user.flags.isAdmin && (req.body.isAdmin !== 'on')) { throw new SiteError(400, "You can't remove your own admin privileges"); } } await userService.updateLocalForAdmin(res.locals.userAccount, req.body); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postUpdateLocalUser', message: 'local user account updated', data: { userAccount: { _id: res.locals.userAccount._id, username: res.locals.userAccount.username, }, }, }); break; case 'ban': if (req.user._id.equals(res.locals.userAccount._id)) { throw new SiteError(400, "You can't ban yourself"); } await userService.ban(res.locals.userAccount); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postUpdateLocalUser', message: 'local user banned from the app', data: { userAccount: { _id: res.locals.userAccount._id, username: res.locals.userAccount.username, }, }, }); break; } res.redirect('/admin/user'); } catch (error) { return next(error); } } async getLocalUserView (req, res, next) { const { comment: commentService } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.recentComments = await commentService.getForAuthor(res.locals.userAccount, res.locals.pagination); res.render('admin/user/form'); } catch (error) { this.log.error('failed to produce user view', { error }); return next(error); } } async getUserArchiveJobView (req, res) { res.locals.adminView = 'user-archive'; res.render('admin/user/archive/job'); } async getArchiveLocalUserConfirm (req, res) { res.locals.adminView = 'user-archive'; res.render('admin/user/archive/confirm'); } async postArchiveAction (req, res, next) { const { logan: loganService, user: userService, } = this.dtp.services; try { switch (req.body.action) { case 'update': loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postArchiveAction', message: 'updating user archive record', data: { archive: { _id: res.locals.archive._id, user: { _id: res.locals.archive.user._id, username: res.locals.archive.user.username, }, }, }, }); await userService.updateArchive(res.locals.archive, req.body); return res.redirect(`/admin/user/archive/${res.locals.archive._id}`); case 'delete-file': loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postArchiveAction', message: 'removing user archive file', data: { archive: { _id: res.locals.archive._id, user: { _id: res.locals.archive.user._id, username: res.locals.archive.user.username, }, }, }, }); await userService.deleteArchiveFile(res.locals.archive); return res.redirect(`/admin/user/archive/${res.locals.archive._id}`); case 'delete': loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postArchiveAction', message: 'removing user archive', data: { archive: { _id: res.locals.archive._id, user: { _id: res.locals.archive.user._id, username: res.locals.archive.user.username, }, }, }, }); await userService.deleteArchive(res.locals.archive); return res.redirect(`/admin/user/archive`); default: // unknown/invalid action break; } throw new SiteError(400, `Invalid user archive action: ${req.body.action}`); } catch (error) { this.log.error('failed to delete archive file', { error }); return next(error); } } async getUserArchiveFile (req, res, next) { const { minio: minioService } = this.dtp.services; try { res.locals.adminView = 'user-archive'; this.log.debug('archive', { archive: res.locals.archive }); const stream = await minioService.openDownloadStream({ bucket: res.locals.archive.archive.bucket, key: res.locals.archive.archive.key, }); res.status(200); res.set('Content-Type', 'application/zip'); res.set('Content-Size', res.locals.archive.archive.size); res.set('Content-Disposition', `attachment; filename="user-${res.locals.archive.user._id}.zip"`); stream.pipe(res); } catch (error) { this.log.error('failed to stream user archive file', { error }); return next(error); } } async getUserArchiveView (req, res) { res.locals.adminView = 'user-archive'; res.render('admin/user/archive/view'); } async getUserArchiveIndex (req, res, next) { const { user: userService } = this.dtp.services; try { res.locals.adminView = 'user-archive'; res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.archive = await userService.getArchives(res.locals.pagination); res.render('admin/user/archive/index'); } catch (error) { this.log.error('failed to render the User archives index', { error }); return next(error); } } async getHomeView (req, res, next) { const { user: userService } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 10); res.locals.userAccounts = await userService.searchLocalUserAccounts(res.locals.pagination, req.query.u); res.locals.totalUserCount = await userService.getTotalCount(); res.render('admin/user/index'); } catch (error) { return next(error); } } } module.exports = { name: 'adminUser', slug: 'admin-user', className: 'UserAdminController', create: async (dtp) => { return new UserAdminController(dtp); }, };