Open source web app engine for the Digital Telepresence Platform.
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.
 
 
 
 

452 lines
14 KiB

// 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 {
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),
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(
'/:userId/otp-setup',
limiterService.createMiddleware(limiterService.config.user.getOtpSetup),
otpSetup,
this.getOtpSetup.bind(this),
);
router.get(
'/:userId/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(
'/:userId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto),
authRequired,
checkProfileOwner,
this.deleteProfilePhoto.bind(this),
);
}
async populateCoreUsername (req, res, next, coreUsername) {
const { 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 });
return next(error);
}
}
async populateCoreUserId (req, res, next, coreUserId) {
const { 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) {
this.log.error('failed to populate core user id', { coreUserId, error });
return next(error);
}
}
async populateLocalUsername (req, res, next, username) {
const { 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 });
return next(error);
}
}
async populateLocalUserId (req, res, next, userId) {
const { 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) {
this.log.error('failed to populate local user id', { 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.username}`);
});
} 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 {
await userService.updatePhoto(req.user, req.file);
const displayList = this.createDisplayList('profile-photo');
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 {
await userService.updateHeaderImage(req.user, req.file);
const displayList = this.createDisplayList('header-image');
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 postUpdateCoreSettings (req, res) {
const { coreNode: coreNodeService } = this.dtp.services;
try {
await coreNodeService.updateUserSettings(req.user, req.body);
const displayList = this.createDisplayList('app-settings');
displayList.reload();
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to update CoreUser settings', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postUpdateSettings (req, res) {
const { user: userService } = this.dtp.services;
try {
await userService.updateSettings(req.user, req.body);
const displayList = this.createDisplayList('app-settings');
displayList.reload();
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 getCoreUserSettingsView (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-core');
} catch (error) {
this.log.error('failed to render CoreUser settings view', { error });
return next(error);
}
}
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 render 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) => { return new UserController(dtp); },
};