// user.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; import { SiteController, SiteError } from '../../lib/site-lib.js'; import { populateUserId, } from './lib/populators.js'; export default class UserController extends SiteController { static get name ( ) { return 'UserController'; } static get slug ( ) { return 'user'; } constructor (dtp) { super(dtp, UserController); } async start ( ) { const { dtp } = this; const { csrfToken: csrfTokenService, limiter: limiterService, otpAuth: otpAuthService, session: sessionService, } = dtp.services; const upload = this.createMulter(UserController.slug, { limits: { fileSize: 1024 * 1000 * 15, }, }); const router = this.createRouter('/user'); const authCheck = 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 = 'Member 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', populateUserId(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(), csrfTokenService.middleware({ name: 'account-settings' }), this.postUpdateSettings.bind(this), ); router.post( '/:userId/block', limiterService.create(limiterService.config.user.postBlockUser), upload.none(), this.postBlockUser.bind(this), ); if (process.env.DTP_MEMBER_SIGNUP === 'enabled') { router.post('/', limiterService.create(limiterService.config.user.postCreateUser), csrfTokenService.middleware({ name: 'signup' }), this.postCreateUser.bind(this), ); } router.get( '/resend-welcome-email', authCheck, limiterService.create(limiterService.config.user.getResendWelcomeEmail), this.getResendWelcomeEmail.bind(this), ); router.get( '/autocomplete', authCheck, this.getAutocomplete.bind(this), ); router.get( '/resolve', authCheck, this.getResolveUsername.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), authCheck, this.getOtpDisable.bind(this), ); router.get( '/:userId/settings', limiterService.create(limiterService.config.user.getSettings), otpMiddleware, checkProfileOwner, this.getUserSettingsView.bind(this), ); router.get( '/:userId/stripe/customer-portal', limiterService.create(limiterService.config.user.getStripeCustomerPortal), checkProfileOwner, this.getStripeCustomerPortal.bind(this), ); router.get( '/:userId/block', limiterService.create(limiterService.config.user.getBlockView), otpMiddleware, checkProfileOwner, this.getBlockView.bind(this), ); router.get( '/:userId', limiterService.create(limiterService.config.user.getUserProfile), otpMiddleware, checkProfileOwner, this.getUserView.bind(this), ); router.delete( '/:userId/profile-photo', limiterService.create(limiterService.config.user.deleteProfilePhoto), checkProfileOwner, this.deleteProfilePhoto.bind(this), ); router.delete( '/:userId/block/:blockedUserId', limiterService.create(limiterService.config.user.deleteBlockedUser), checkProfileOwner, this.deleteBlockedUser.bind(this), ); } async postCreateUser (req, res, next) { const { user: userService } = this.dtp.services; try { // If the user session doesn't have a signup captcha, then they did not // request the signup form and are most likely an automated bot if (!req.session.dtp || !req.session.dtp.captcha || !req.session.dtp.captcha.signup) { throw new SiteError(403, 'Invalid session state'); } // Reject the request if the captcha text entered does not exactly match // the text generated if (req.body.captcha !== req.session.dtp.captcha.signup) { throw new SiteError(403, 'The captcha text entered does not match'); } // The captcha matches. Create the user account and sign them in. res.locals.user = await userService.create(req.body); req.login(res.locals.user, (error) => { if (error) { return next(error); } res.redirect('/'); // send them to the home page }); } 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'); this.log.info('updating user profile photo', { user: req.user._id, file: req.file }); await userService.updatePhoto(req.user, req.file); displayList.reloadView(); 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 postUpdateSettings (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('app-settings'); await userService.setUserSettings(req.user, req.body); this.log.info('user settings updated', { username: req.user.username, newUsername: req.body.username, }); if (req.user.ui.theme !== req.body.uiTheme) { displayList.reloadView(); } else { 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 postBlockUser (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('block-user'); await userService.block(req.user, req.body.userId); displayList.removeElement(`.streamray-status[data-author-id="${req.body.userId}"]`); displayList.showNotification( `Member blocked successfully`, 'success', 'bottom-left', 3000, ); return res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to block user', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getResendWelcomeEmail (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('welcome-email'); await userService.sendWelcomeEmail(req.user); displayList.showNotification( 'Welcome email sent (check spam folder)', 'success', 'bottom-center', 4000, ); res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to resend welcome email', { error }); res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getAutocomplete (req, res) { const { user: userService } = this.dtp.services; try { if (!req.query || !req.query.q || (req.query.q.length === 0)) { throw new SiteError(406, "Username must not be empty"); } const strings = await userService.autocomplete({ skip: 0, cpp: 8 }, req.query.q); res.status(200).json({ success: true, strings }); } catch (error) { this.log.error('failed to autocomplete username', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getResolveUsername (req, res) { const { user: userService } = this.dtp.services; try { const user = await userService.lookup(req.query.q); res.status(200).json({ success: true, user }); } catch (error) { this.log.error('failed to resolve username', { 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 getStripeCustomerPortal (req, res, next) { const { stripe: stripeService } = this.dtp.services; /* * Stripe Integration */ try { const session = await stripeService.createCustomerPortalSession(req.user); return res.redirect(303, session.url); } catch (error) { this.log.error('failed to provide Stripe Customer Portal session', { error }); return next(error); } } async getUserSettingsView (req, res, next) { const { csrfToken: csrfTokenService, otpAuth: otpAuthService, } = this.dtp.services; try { res.locals.csrfTokenAccountSettings = await csrfTokenService.create(req, { name: 'account-settings', expiresMinutes: 20, }); res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account'); res.render('user/settings'); } catch (error) { this.log.error('failed to produce user settings view', { error }); return next(error); } } async getBlockView (req, res, next) { const { user: userService } = this.dtp.services; try { res.locals.blockedUsers = await userService.getBlockedUsers(req.user._id); res.render('user/block-list'); } catch (error) { this.log.error('failed to present block list view', { error }); return next(error); } } async getUserView (req, res, next) { const { handcash: handcashService } = this.dtp.services; /* * HandCash Integration */ try { res.locals.handcash = { connectUrl: handcashService.getRedirectionUrl(req.user), }; res.locals.handcash.profile = await handcashService.getUserProfile(req.user); } catch (error) { // this.log.debug('authenticated user does not have a HandCash wallet connection', { error }); // fall through } try { 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.reloadView(); 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 deleteBlockedUser (req, res) { const { user: userService } = this.dtp.services; try { const displayList = this.createDisplayList('block-user'); await userService.unblock(req.user, req.params.blockedUserId); displayList.removeElement(`li[data-user-id="${req.params.blockedUserId}"]`); displayList.showNotification( `Member un-blocked successfully`, 'success', 'bottom-left', 3000, ); return res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to un-block user', { error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } }