DTP Base provides a scalable and secure Node.js application development harness ready for production service.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

466 lines
14 KiB

// user.js
// Copyright (C) 2024 DTP Technologies, LLC
// All Rights Reserved
'use strict';
import express from 'express';
import mongoose from 'mongoose';
import multer from 'multer';
import { SiteController, SiteError } from '../../lib/site-lib.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 = multer({ dest: `/tmp/dtp/${this.dtp.config.site.domainKey}/uploads/${UserController.name}` });
const router = express.Router();
dtp.app.use('/user', router);
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', 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(),
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 populateUser (req, res, next, userId) {
const { user: userService } = this.dtp.services;
if (!req.user) {
return res.redirect('/welcome');
}
if (!mongoose.Types.ObjectId.isValid(userId)) {
return next(new SiteError(406, 'Invalid User'));
}
try {
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 {
// 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,
});
}
}
}