Browse Source

Core vs. Local user updates (there will be more)

develop
Rob Colbert 2 years ago
parent
commit
98853ced33
  1. 9
      README.md
  2. 2
      app/controllers/admin/user.js
  3. 2
      app/controllers/hive/user.js
  4. 97
      app/controllers/user.js
  5. 7
      app/models/user.js
  6. 15
      app/services/chat.js
  7. 3
      app/services/session.js
  8. 213
      app/services/user.js
  9. 2
      app/views/admin/core-user/form.pug
  10. 2
      app/views/admin/user/form.pug
  11. 2
      app/views/components/navbar.pug
  12. 2
      app/views/components/off-canvas.pug
  13. 10
      lib/site-ioserver.js

9
README.md

@ -2,6 +2,15 @@
The base project from which all others are forked when working with my framework and system. You don't have to start from this project at all. BUT, it can save you a lot of time by simply being a 100% compatible base on which you can build your apps and sites.
## Host Preparation
The following commands must be exeucted on any host expected to run DTP Framework applications.
```sh
apt -y update && apt -y upgrade
apt -y install linux-headers-generic linux-headers-virtual linux-image-virtual linux-virtual
apt -y install build-essential ffmpeg supervisor
```
## Install Data Tier Components
You will need MongoDB and MinIO installed and running before you can start DTP Base web services.

2
app/controllers/admin/user.js

@ -68,7 +68,7 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.userAccounts = await userService.getUserAccounts(res.locals.pagination, req.query.u);
res.locals.userAccounts = await userService.searchLocalUserAccounts(res.locals.pagination, req.query.u);
res.locals.totalUserCount = await userService.getTotalCount();
res.render('admin/user/index');
} catch (error) {

2
app/controllers/hive/user.js

@ -78,7 +78,7 @@ class HiveUserController extends SiteController {
throw new SiteError(406, 'Must include search term');
}
res.locals.q = await userService.filterUsername(req.query.q);
res.locals.q = userService.filterUsername(req.query.q);
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.userProfiles = await userService.getUserAccounts(res.locals.pagination, res.locals.q);
res.locals.userProfiles = res.locals.userProfiles.map((user) => {

97
app/controllers/user.js

@ -60,8 +60,10 @@ class UserController extends SiteController {
return next();
}
router.param('username', this.populateUsername.bind(this));
router.param('userId', this.populateUserId.bind(this));
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(
@ -73,7 +75,7 @@ class UserController extends SiteController {
);
router.post(
'/:userId/profile-photo',
'/:localUserId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.postProfilePhoto),
checkProfileOwner,
upload.single('imageFile'),
@ -81,7 +83,7 @@ class UserController extends SiteController {
);
router.post(
'/:userId/settings',
'/:localUserId/settings',
limiterService.createMiddleware(limiterService.config.user.postUpdateSettings),
checkProfileOwner,
upload.none(),
@ -124,7 +126,7 @@ class UserController extends SiteController {
);
router.get(
'/:userId/settings',
'/:localUsername/settings',
limiterService.createMiddleware(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
@ -132,7 +134,7 @@ class UserController extends SiteController {
this.getUserSettingsView.bind(this),
);
router.get(
'/:username',
'/:localUsername',
limiterService.createMiddleware(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
@ -148,48 +150,84 @@ class UserController extends SiteController {
);
}
async populateUsername (req, res, next, username) {
async populateCoreUsername (req, res, next, coreUsername) {
const { user: userService } = this.dtp.services;
try {
res.locals.userProfile = await userService.getPublicProfile('User', username);
if (!res.locals.userProfile) {
throw new SiteError(404, 'Member not found');
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');
}
return next();
// 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 username with public profile', { username, error });
this.log.error('failed to populate core username', { coreUsername, error });
return next(error);
}
}
async populateUserId (req, res, next, userId) {
async populateCoreUserId (req, res, next, coreUserId) {
const { user: userService } = this.dtp.services;
try {
userId = mongoose.Types.ObjectId(userId);
} catch (error) {
return next(new SiteError(406, 'Invalid User'));
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);
}
try {
res.locals.userProfile = await userService.getUserAccount(userId);
if (!res.locals.userProfile) {
throw new SiteError(404, 'Core member not found');
}
return next();
} catch (error) {
this.log.error('failed to populate userId', { userId, error });
this.log.error('failed to populate core user id', { coreUserId, error });
return next(error);
}
}
async populateCoreUserId (req, res, next, coreUserId) {
const { coreNode: coreNodeService } = this.dtp.services;
async populateLocalUsername (req, res, next, username) {
const { user: userService } = this.dtp.services;
try {
coreUserId = mongoose.Types.ObjectId(coreUserId);
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) {
return next(new SiteError(406, 'Invalid User'));
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.userProfile = await coreNodeService.getUserByLocalId(coreUserId);
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 coreUserId', { coreUserId, error });
this.log.error('failed to populate local user id', { userId, error });
return next(error);
}
}
@ -229,8 +267,9 @@ class UserController extends SiteController {
async postProfilePhoto (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('profile-photo');
await userService.updatePhoto(req.user, req.file);
const displayList = this.createDisplayList('profile-photo');
displayList.showNotification(
'Profile photo updated successfully.',
'success',
@ -250,8 +289,9 @@ class UserController extends SiteController {
async postHeaderImage (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('header-image');
await userService.updateHeaderImage(req.user, req.file);
const displayList = this.createDisplayList('header-image');
displayList.showNotification(
'Header image updated successfully.',
'success',
@ -271,10 +311,9 @@ class UserController extends SiteController {
async postUpdateCoreSettings (req, res) {
const { coreNode: coreNodeService } = this.dtp.services;
try {
const displayList = this.createDisplayList('app-settings');
await coreNodeService.updateUserSettings(req.user, req.body);
const displayList = this.createDisplayList('app-settings');
displayList.reload();
res.status(200).json({ success: true, displayList });
} catch (error) {

7
app/models/user.js

@ -22,17 +22,18 @@ const {
const UserSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1 },
email: { type: String, required: true, lowercase: true, unique: true },
email: { type: String, required: true, lowercase: true, unique: true, select: false },
username: { type: String, required: true },
username_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 },
passwordSalt: { type: String, required: true },
password: { type: String, required: true },
passwordSalt: { type: String, required: true, select: false },
password: { type: String, required: true, select: false },
displayName: { type: String },
bio: { type: String, maxlength: 300 },
picture: {
large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' },
},
header: { type: Schema.ObjectId, ref: 'Image' },
badges: { type: [String] },
flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false },

15
app/services/chat.js

@ -473,7 +473,12 @@ class ChatService extends SiteService {
async createMessage (author, messageDefinition) {
const { sticker: stickerService, user: userService } = this.dtp.services;
author = await userService.getUserAccount(author._id);
this.log.alert('user record', { author });
if (author.type === 'User') {
author = await userService.getLocalUserAccount(author._id);
} else {
author = await userService.getCoreUserAccount(author._id);
}
if (!author || !author.permissions || !author.permissions.canChat) {
throw new SiteError(403, `You are not permitted to chat at all on ${this.dtp.config.site.name}`);
}
@ -744,7 +749,13 @@ class ChatService extends SiteService {
const { user: userService } = this.dtp.services;
const NOW = new Date();
const userCheck = await userService.getUserAccount(user._id);
let userCheck;
if (user.type === '') {
userCheck = await userService.getLocalUserAccount(user._id);
} else {
userCheck = await userService.getCoreUserAccount(user._id);
}
if (!userCheck || !userCheck.permissions || !userCheck.permissions.canChat) {
throw new SiteError(403, 'You are not permitted to chat');
}

3
app/services/session.js

@ -92,8 +92,9 @@ class SessionService extends SiteService {
delete user.stats._id;
delete user.optIn._id;
break;
case 'local':
user = await userService.getUserAccount(userId);
user = await userService.getLocalUserAccount(userId);
user.type = 'User';
break;
}

213
app/services/user.js

@ -20,6 +20,12 @@ const uuidv4 = require('uuid').v4;
const { SiteError, SiteService } = require('../../lib/site-lib');
/*
* The entire concept of "get a user" is in flux right now. It's best to just
* ignore what's happening in this service right now, and focus on other
* features in the sytem.
*/
class UserService extends SiteService {
constructor (dtp) {
@ -174,7 +180,7 @@ class UserService extends SiteService {
async emailOptOut (userId, category) {
userId = mongoose.Types.ObjectId(userId);
const user = await this.getUserAccount(userId);
const user = await this.getLocalUserAccount(userId);
if (!user) {
throw new SiteError(406, 'Invalid opt-out token');
}
@ -199,7 +205,6 @@ class UserService extends SiteService {
throw SiteError(403, 'Invalid user account operation');
}
// strip characters we don't want to allow in username
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -221,7 +226,6 @@ class UserService extends SiteService {
}
async updateForAdmin (user, userDefinition) {
// strip characters we don't want to allow in username
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -265,7 +269,6 @@ class UserService extends SiteService {
const updateOp = { $set: { }, $unset: { } };
// strip characters we don't want to allow in username
updateOp.$set.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
if (!updateOp.$set.username || (updateOp.$set.username.length === 0)) {
throw new SiteError(400, 'Must include a username');
@ -303,7 +306,7 @@ class UserService extends SiteService {
}, options);
const accountEmail = account.username.trim().toLowerCase();
const accountUsername = await this.filterUsername(accountEmail);
const accountUsername = this.filterUsername(accountEmail);
this.log.debug('locating user record', { accountEmail, accountUsername });
const user = await User
@ -393,28 +396,23 @@ class UserService extends SiteService {
);
}
filterUserObject (user) {
const filteredUser = {
_id: user._id,
created: user.created,
displayName: user.displayName,
username: user.username,
username_lc: user.username_lc,
bio: user.bio,
flags: user.flags,
permissions: user.permissions,
picture: user.picture,
};
if (filteredUser.flags && filteredUser.flags._id) {
delete filteredUser.flags._id;
async getLocalUserId (username) {
const user = await User.findOne({ username_lc: username }).select('_id').lean();
if (!user) {
return; // undefined
}
if (filteredUser.permissions && filteredUser.permissions._id) {
delete filteredUser.permissions._id;
return user._id;
}
return filteredUser;
async getCoreUserId (username) {
const user = await CoreUser.findOne({ username_lc: username }).select('_id').lean();
if (!user) {
return; // undefined
}
return user._id;
}
async getUserAccount (userId) {
async getLocalUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +optIn +picture')
@ -427,11 +425,47 @@ class UserService extends SiteService {
return user;
}
async getUserAccounts (pagination, username) {
async getCoreUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +optIn +picture')
.populate(this.populateUser)
.lean();
if (!user) {
throw new SiteError(404, 'Core member account not found');
}
user.type = 'CoreUser';
return user;
}
async getLocalUserProfile (userId) {
const user = await User
.findById(userId)
.select('+email +flags +settings')
.populate(this.populateUser)
.lean();
user.type = 'User';
return user;
}
async getCoreUserProfile (userId) {
const user = await CoreUser
.findById(userId)
.select('+core +flags +settings')
.populate(this.populateUser)
.lean();
user.type = 'CoreUser';
return user;
}
async searchLocalUserAccounts (pagination, username) {
let search = { };
if (username) {
username = this.filterUsername(username);
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
const users = await User
.find(search)
.sort({ username_lc: 1 })
@ -444,60 +478,24 @@ class UserService extends SiteService {
return users.map((user) => { user.type = 'User'; return user; });
}
async getUserProfile (userId) {
let user;
try {
userId = mongoose.Types.ObjectId(userId); // will throw if invalid format
user = User.findById(userId);
} catch (error) {
user = User.findOne({ username: userId });
}
user = await user
.select('+email +flags +settings')
.populate(this.populateUser)
.lean();
return user;
}
async getPublicProfile (type, username) {
if (!username || (typeof username !== 'string')) {
throw new SiteError(406, 'Invalid username');
}
username = username.trim().toLowerCase();
if (username.length === 0) {
throw new SiteError(406, 'Invalid username');
}
let user;
switch (type) {
case 'CoreUser':
user = await CoreUser
.findOne({ username_lc: username })
.select('_id created username username_lc displayName bio picture header core')
.populate(this.populateUser)
.lean();
if (user) {
user.type = 'CoreUser';
}
break;
async searchCoreUserAccounts (pagination, username) {
let search = { };
case 'User':
user = await User
.findOne({ username_lc: username })
.select('_id created username username_lc displayName bio picture header')
.populate(this.populateUser)
.lean();
if (user) {
user.type = 'User';
username = this.filterUsername(username);
if (username) {
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
break;
default:
throw new SiteError(400, 'Invalid user account type');
}
const users = await CoreUser
.find(search)
.sort({ username_lc: 1 })
.select('+core +coreUserId +flags +permissions +optIn')
.skip(pagination.skip)
.limit(pagination.cpp)
.lean()
;
return user;
return users.map((user) => { user.type = 'CoreUser'; return user; });
}
async getRecent (maxCount = 3) {
@ -579,10 +577,6 @@ class UserService extends SiteService {
return actions;
}
async filterUsername (username) {
return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
}
async checkUsername (username) {
if (!username || (typeof username !== 'string') || (username.length === 0)) {
throw new SiteError(406, 'Invalid username');
@ -598,6 +592,34 @@ class UserService extends SiteService {
}
}
filterUsername (username) {
while (username[0] === '@') {
username = username.slice(1);
}
return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
}
filterUserObject (user) {
const filteredUser = {
_id: user._id,
created: user.created,
displayName: user.displayName,
username: user.username,
username_lc: user.username_lc,
bio: user.bio,
flags: user.flags,
permissions: user.permissions,
picture: user.picture,
};
if (filteredUser.flags && filteredUser.flags._id) {
delete filteredUser.flags._id;
}
if (filteredUser.permissions && filteredUser.permissions._id) {
delete filteredUser.permissions._id;
}
return filteredUser;
}
async recordProfileView (user, req) {
const { resource: resourceService } = this.dtp.services;
await resourceService.recordView(req, 'User', user._id);
@ -653,6 +675,41 @@ class UserService extends SiteService {
await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } });
}
async updateHeaderImage (user, file) {
const { image: imageService } = this.dtp.services;
await this.removeHeaderImage(user.header);
const images = [
{
width: 1400,
height: 400,
format: 'jpeg',
formatParameters: {
quality: 80,
},
},
];
await imageService.processImageFile(user, file, images);
await User.updateOne(
{ _id: user._id },
{
$set: {
'header': images[0].image._id,
},
},
);
}
async removeHeaderImage (user) {
const { image: imageService } = this.dtp.services;
user = await this.getUserAccount(user._id);
if (user.header) {
await imageService.deleteImage(user.header);
}
await User.updateOne({ _id: user._id }, { $unset: { 'header': '' } });
}
async blockUser (user, blockedUser) {
if (user._id.equals(blockedUser._id)) {
throw new SiteError(406, "You can't block yourself");

2
app/views/admin/core-user/form.pug

@ -13,7 +13,7 @@ block content
if userAccount.displayName
.uk-text-large= userAccount.displayName
div
a(href=`/user/${userAccount._id}`) @#{userAccount.username}
a(href=`/user/${userAccount.username}`) @#{userAccount.username}
.uk-card-body
.uk-margin

2
app/views/admin/user/form.pug

@ -20,7 +20,7 @@ block content
.uk-width-auto
a(href=`mailto:${userAccount.email}`)= userAccount.email
.uk-width-auto
a(href=`/user/${userAccount._id}`) @#{userAccount.username}
a(href=`/user/${userAccount.username}`) @#{userAccount.username}
.uk-card-body
.uk-margin

2
app/views/components/navbar.pug

@ -52,7 +52,7 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
i.fas.fa-user
span Profile
li
a(href= user.core ? `/user/core/${user._id}/settings` : `/user/${user._id}/settings`)
a(href= user.core ? `/user/core/${user.username}/settings` : `/user/${user.username}/settings`)
span.nav-item-icon
i.fas.fa-cog
span Settings

2
app/views/components/off-canvas.pug

@ -48,7 +48,7 @@ mixin renderMenuItem (iconClass, label)
.uk-width-expand Profile
li(class={ "uk-active": (currentView === 'user-settings') })
a(href=`/user/${user._id}/settings`).uk-display-block
a(href=`/user/${user.username}/settings`).uk-display-block
div(uk-grid).uk-grid-collapse
.uk-width-auto
.app-menu-icon

10
lib/site-ioserver.js

@ -15,12 +15,12 @@ const ConnectToken = mongoose.model('ConnectToken');
const marked = require('marked');
const { SiteLog } = require(path.join(__dirname, 'site-log'));
const { SiteCommon } = require(path.join(__dirname, 'site-common'));
const Events = require('events');
class SiteIoServer extends Events {
class SiteIoServer extends SiteCommon {
constructor (dtp) {
super();
super(dtp, { name: 'ioServer', slug: 'io-server' });
this.dtp = dtp;
this.log = new SiteLog(dtp, DTP_COMPONENT);
}
@ -74,6 +74,10 @@ class SiteIoServer extends Events {
}
async stop ( ) {
if (this.io) {
this.io.close();
delete this.io;
}
}

Loading…
Cancel
Save