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.
481 lines
14 KiB
481 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 });
|
|
const image = await userService.updatePhoto(req.user, req.file);
|
|
displayList.showNotification(
|
|
'Profile photo updated successfully.',
|
|
'success',
|
|
'bottom-center',
|
|
2000,
|
|
);
|
|
displayList.setAttribute(
|
|
'#profile-picture-file',
|
|
'src',
|
|
`/image/${image._id}`,
|
|
);
|
|
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.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 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,
|
|
});
|
|
}
|
|
}
|
|
}
|