// user.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const express = require('express'); const mongoose = require('mongoose'); const { SiteController, SiteError } = require('../../lib/site-lib'); class UserController extends SiteController { constructor (dtp) { super(dtp, module.exports); } async start ( ) { const { dtp } = this; const { csrfToken: csrfTokenService, limiter: limiterService, otpAuth: otpAuthService, session: sessionService, } = dtp.services; const upload = this.createMulter('user', { limits: { fileSize: 1024 * 1000 * 5, // 5MB }, }); 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.username}`; }, }); const otpMiddleware = otpAuthService.middleware('Account', { adminRequired: false, otpRequired: false, otpRedirectURL: async (req) => { return `/user/${req.user.username}`; }, }); 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('localUsername', this.populateLocalUsername.bind(this)); router.param('coreUsername', this.populateCoreUsername.bind(this)); router.param('localUserId', this.populateLocalUserId.bind(this)); router.param('coreUserId', this.populateCoreUserId.bind(this)); router.post( '/core/:coreUserId/settings', limiterService.createMiddleware(limiterService.config.user.postUpdateCoreSettings), checkProfileOwner, upload.none(), this.postUpdateCoreSettings.bind(this), ); router.post( '/:localUserId/profile-photo', limiterService.createMiddleware(limiterService.config.user.postProfilePhoto), checkProfileOwner, upload.single('imageFile'), this.postProfilePhoto.bind(this), ); router.post( '/:localUserId/settings', limiterService.createMiddleware(limiterService.config.user.postUpdateSettings), checkProfileOwner, upload.none(), this.postUpdateSettings.bind(this), ); router.post( '/', limiterService.createMiddleware(limiterService.config.user.postCreate), csrfTokenService.middleware({ name: 'user-create' }), this.postCreateUser.bind(this), ); router.get( '/core/:coreUserId/settings', limiterService.createMiddleware(limiterService.config.user.getSettings), authRequired, otpMiddleware, checkProfileOwner, this.getCoreUserSettingsView.bind(this), ); router.get( '/core/:coreUserId', limiterService.createMiddleware(limiterService.config.user.getUserProfile), authRequired, otpMiddleware, this.getUserView.bind(this), ); router.get( '/:localUserId/otp-setup', limiterService.createMiddleware(limiterService.config.user.getOtpSetup), otpSetup, this.getOtpSetup.bind(this), ); router.get( '/:localUserId/otp-disable', limiterService.createMiddleware(limiterService.config.user.getOtpDisable), authRequired, this.getOtpDisable.bind(this), ); router.get( '/:localUsername/settings', limiterService.createMiddleware(limiterService.config.user.getSettings), authRequired, otpMiddleware, checkProfileOwner, this.getUserSettingsView.bind(this), ); router.get( '/:localUsername', limiterService.createMiddleware(limiterService.config.user.getUserProfile), authRequired, otpMiddleware, this.getUserView.bind(this), ); router.delete( '/:localUserId/profile-photo', limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto), authRequired, checkProfileOwner, this.deleteProfilePhoto.bind(this), ); } async populateCoreUsername (req, res, next, coreUsername) { const { logan: loganService, user: userService, } = this.dtp.services; try { res.locals.username = userService.filterUsername(coreUsername); res.locals.userProfileId = await userService.getCoreUserId(res.locals.username); if (!res.locals.userProfileId) { throw new SiteError(404, 'Core member not found'); } // manually chain over to the ID parameter resolver return this.populateCoreUserId(req, res, next, res.locals.userProfileId); } catch (error) { this.log.error('failed to populate core username', { coreUsername, error }); loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'populateCoreUsername', message: `failed to populate Core user: ${error.message}`, data: { coreUsername, error }, }); return next(error); } } async populateCoreUserId (req, res, next, coreUserId) { const { logan: loganService, user: userService, } = this.dtp.services; try { res.locals.userProfileId = mongoose.Types.ObjectId(coreUserId); if (req.user && (req.user.type === 'CoreUser') && req.user._id.equals(res.locals.userProfileId)) { res.locals.userProfile = await userService.getCoreUserAccount(res.locals.userProfileId); } else { res.locals.userProfile = await userService.getCoreUserProfile(res.locals.userProfileId); } if (!res.locals.userProfile) { throw new SiteError(404, 'Core member not found'); } return next(); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'populateCoreUserId', message: `failed to populate core user: ${error.message}`, data: { coreUserId, error }, }); return next(error); } } async populateLocalUsername (req, res, next, username) { const { logan: loganService, user: userService, } = this.dtp.services; try { res.locals.username = userService.filterUsername(username); res.locals.userProfileId = await userService.getLocalUserId(res.locals.username); if (!res.locals.userProfileId) { throw new SiteError(404, 'Local member not found'); } if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) { res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId); } else { res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId); } return next(); } catch (error) { this.log.error('failed to populate local username', { username, error }); loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'populateLocalUsername', message: `failed to populate local user: ${error.message}`, data: { username, error }, }); return next(error); } } async populateLocalUserId (req, res, next, userId) { const { logan: loganService, user: userService, } = this.dtp.services; try { res.locals.userProfileId = mongoose.Types.ObjectId(userId); if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) { res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId); } else { res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId); } if (!res.locals.userProfile) { throw new SiteError(404, 'Local member not found'); } return next(); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'populateLocalUserId', message: `failed to populate local user: ${error.message}`, data: { userId, error }, }); return next(error); } } async postCreateUser (req, res, next) { const { logan: loganService, 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); const form = Object.assign(req.body); if (form.password) { delete form.password; } if (form.passwordv) { delete form.passwordv; } loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postCreateUser', data: { form, user: { _id: res.locals.user._id, username: res.locals.user.username, }, }, }); // log the user in req.login(res.locals.user, (error) => { if (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postCreateUser', message: `failed to start user session: ${error.message}`, data: { error }, }); return next(error); } loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postCreateUser', message: 'user session started', }); res.redirect(`/user/${res.locals.user.username}`); }); } 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); } } async postProfilePhoto (req, res) { const { logan: loganService, user: userService, } = this.dtp.services; try { await userService.updatePhoto(req.user, req.file); const displayList = this.createDisplayList('profile-photo'); displayList.showNotification( 'Profile photo updated successfully.', 'success', 'bottom-center', 2000, ); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postProfilePhoto', message: 'profile photo updated', }); res.status(200).json({ success: true, displayList }); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postProfilePhoto', message: `failed to update profile photo: ${error.message}`, data: { error }, }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async postHeaderImage (req, res) { const { logan: loganService, user: userService, } = this.dtp.services; try { await userService.updateHeaderImage(req.user, req.file); const displayList = this.createDisplayList('header-image'); displayList.showNotification( 'Header image updated successfully.', 'success', 'bottom-center', 2000, ); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postHeaderImage', message: 'header image updated', }); res.status(200).json({ success: true, displayList }); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postHeaderImage', message: `failed to update header image: ${error.message}`, data: { error }, }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async postUpdateCoreSettings (req, res) { const { coreNode: coreNodeService, logan: loganService, } = this.dtp.services; try { await coreNodeService.updateUserSettings(req.user, req.body); const displayList = this.createDisplayList('app-settings'); displayList.reload(); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postUpdateCoreSettings', message: 'CoreUser settings updated', }); res.status(200).json({ success: true, displayList }); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postUpdateCoreSettings', message: `failed to update CoreUser settings: ${error.message}`, data: { error }, }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async postUpdateSettings (req, res) { const { logan: loganService, user: userService, } = this.dtp.services; try { await userService.updateSettings(req.user, req.body); const displayList = this.createDisplayList('app-settings'); displayList.reload(); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postUpdateSettings', message: 'account settings updates', }); res.status(200).json({ success: true, displayList }); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postUpdateSettings', message: `failed to update account settings: ${error.message}`, data: { 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 { logan: loganService, otpAuth: otpAuthService, } = this.dtp.services; try { await otpAuthService.destroyOtpSession(req, 'Account'); await otpAuthService.removeForUser(req.user, 'Account'); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'getOtpDisable', message: 'one-time passwords disabled', }); res.render('user/otp-disabled'); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getOtpDisable', message: `failed to disable OTP: ${error.message}`, data: { error }, }); res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getCoreUserSettingsView (req, res, next) { const { logan: loganService, 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-core'); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getCoreUserSettingsView', message: `failed to render the view: ${error.message}`, data: { error }, }); return next(error); } } async getUserSettingsView (req, res, next) { const { logan: loganService, 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) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getUserSettingsView', message: `failed to render the view: ${error.message}`, data: { error }, }); return next(error); } } async getUserView (req, res, next) { const { comment: commentService, logan: loganService, } = 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) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getUserView', message: `failed to render the view: ${error.message}`, data: { error }, }); return next(error); } } async deleteProfilePhoto (req, res) { const { logan: loganService, 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, ); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'deleteProfilePhoto', message: 'profile photo removed', }); res.status(200).json({ success: true, displayList }); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'deleteProfilePhoto', message: `failed to remove profile photo: ${error.message}`, data: { error }, }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async deleteHeaderImage (req, res) { const { logan: loganService, 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, ); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'deleteHeaderImage', message: 'profile header image removed', }); res.status(200).json({ success: true, displayList }); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'deleteHeaderImage', message: `failed to remove header image: ${error.message}`, data: { error }, }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } } module.exports = { logId: 'ctl:user', index: 'user', className: 'UserController', create: async (dtp) => { return new UserController(dtp); }, };