// user.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const DTP_COMPONENT_NAME = 'user'; const express = require('express'); const mongoose = require('mongoose'); const multer = require('multer'); const { SiteController, SiteError } = require('../../lib/site-lib'); class UserController extends SiteController { constructor (dtp) { super(dtp, DTP_COMPONENT_NAME); } async start ( ) { const { dtp } = this; const { limiter: limiterService, otpAuth: otpAuthService, session: sessionService, } = dtp.services; const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}` }); const router = express.Router(); dtp.app.use('/user', router); const authRequired = sessionService.authCheckMiddleware({ requireLogin: true }); const otpSetup = otpAuthService.middleware('Account', { adminRequired: false, otpRequired: true, otpRedirectURL: async (req) => { return `/user/${req.user._id}`; }, }); const otpMiddleware = otpAuthService.middleware('Account', { adminRequired: false, otpRequired: false, otpRedirectURL: async (req) => { return `/user/${req.user._id}`; }, }); router.use( async (req, res, next) => { try { res.locals.currentView = 'user'; res.locals.pageTitle = 'Manage your user account.'; return next(); } catch (error) { return next(error); } }, ); async function checkProfileOwner (req, res, next) { if (!req.user || !req.user._id.equals(res.locals.userProfile._id)) { return next(new SiteError(403, 'This is not your user account or profile')); } return next(); } router.param('userId', this.populateUser.bind(this)); router.post( '/:userId/profile-photo', limiterService.create(limiterService.config.user.postProfilePhoto), checkProfileOwner, upload.single('imageFile'), this.postProfilePhoto.bind(this), ); router.post( '/:userId/settings', limiterService.create(limiterService.config.user.postUpdateSettings), checkProfileOwner, upload.none(), this.postUpdateSettings.bind(this), ); router.post( '/', limiterService.create(limiterService.config.user.postCreate), this.postCreateUser.bind(this), ); router.get( '/:userId/otp-setup', limiterService.create(limiterService.config.user.getOtpSetup), otpSetup, this.getOtpSetup.bind(this), ); router.get( '/:userId/otp-disable', limiterService.create(limiterService.config.user.getOtpDisable), authRequired, this.getOtpDisable.bind(this), ); router.get( '/:userId/settings', limiterService.create(limiterService.config.user.getSettings), authRequired, otpMiddleware, checkProfileOwner, this.getUserSettingsView.bind(this), ); router.get( '/:userId', limiterService.create(limiterService.config.user.getUserProfile), authRequired, otpMiddleware, checkProfileOwner, this.getUserView.bind(this), ); router.delete( '/:userId/profile-photo', limiterService.create(limiterService.config.user.deleteProfilePhoto), authRequired, checkProfileOwner, this.deleteProfilePhoto.bind(this), ); } async populateUser (req, res, next, userId) { const { user: userService } = this.dtp.services; try { userId = mongoose.Types.ObjectId(userId); } catch (error) { return next(new SiteError(406, 'Invalid User')); } try { if (!req.user._id.equals(userId)) { return next(new Error('Invalid account ID')); } res.locals.userProfile = await userService.getUserAccount(userId); return next(); } catch (error) { this.log.error('failed to populate userId', { userId, error }); return next(error); } } async postCreateUser (req, res, next) { const { user: userService } = this.dtp.services; try { // verify that the request has submitted a captcha if ((typeof req.body.captcha !== 'string') || req.body.captcha.length === 0) { throw new SiteError(403, 'Invalid signup attempt'); } // verify that the session has a signup captcha if (!req.session.captcha || !req.session.captcha.signup) { throw new SiteError(403, 'Invalid signup attempt'); } // verify that the captcha from the form matches the captcha in the signup session flow if (req.body.captcha !== req.session.captcha.signup) { throw new SiteError(403, 'The captcha value is not correct'); } // create the user account res.locals.user = await userService.create(req.body); // log the user in req.login(res.locals.user, (error) => { if (error) { return next(error); } res.redirect(`/user/${res.locals.user._id}`); }); } catch (error) { this.log.error('failed to create new user', { error }); return next(error); } } async postProfilePhoto (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('profile-photo'); await userService.updatePhoto(req.user, req.file); 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 profile photo', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async postHeaderImage (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('header-image'); await userService.updateHeaderImage(req.user, req.file); displayList.showNotification( 'Header image updated successfully.', 'success', 'bottom-center', 2000, ); res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to update header image', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async postUpdateSettings (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('app-settings'); await userService.updateSettings(req.user, req.body); displayList.showNotification( 'Member account settings updated successfully.', 'success', 'bottom-center', 6000, ); res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to update account settings', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getOtpSetup (req, res) { res.render('user/otp-setup-complete'); } async getOtpDisable (req, res) { const { otpAuth: otpAuthService } = this.dtp.services; try { await otpAuthService.destroyOtpSession(req, 'Account'); await otpAuthService.removeForUser(req.user, 'Account'); res.render('user/otp-disabled'); } catch (error) { this.log.error('failed to disable OTP service for Account', { error }); res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getUserSettingsView (req, res, next) { const { otpAuth: otpAuthService } = this.dtp.services; try { res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account'); res.locals.startTab = req.query.st || 'watch'; res.render('user/settings'); } catch (error) { this.log.error('failed to produce user settings view', { error }); return next(error); } } async getUserView (req, res, next) { const { comment: commentService } = this.dtp.services; try { res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.commentHistory = await commentService.getForAuthor(req.user, res.locals.pagination); res.render('user/profile'); } catch (error) { this.log.error('failed to produce user profile view', { error }); return next(error); } } async deleteProfilePhoto (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('app-settings'); await userService.removePhoto(req.user); displayList.showNotification( 'Profile photo removed successfully.', 'success', 'bottom-center', 2000, ); res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to remove profile photo', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async deleteHeaderImage (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('remove-header-image'); await userService.removeHeaderImage(req.user); displayList.showNotification( 'Header image removed successfully.', 'success', 'bottom-center', 2000, ); res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to remove header image', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } } module.exports = { slug: 'user', name: 'user', create: async (dtp) => { let controller = new UserController(dtp); return controller; }, };