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.
776 lines
21 KiB
776 lines
21 KiB
// user.js
|
|
// Copyright (C) 2024 DTP Technologies, LLC
|
|
// All Rights Reserved
|
|
|
|
'use strict';
|
|
|
|
import path from 'node:path';
|
|
|
|
import mongoose from 'mongoose';
|
|
const User = mongoose.model('User');
|
|
|
|
import passport from 'passport';
|
|
import PassportLocal from 'passport-local';
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
import { SiteService, SiteError } from '../../lib/site-lib.js';
|
|
|
|
export default class UserService extends SiteService {
|
|
|
|
static get name ( ) { return 'UserService'; }
|
|
static get slug ( ) { return 'user'; }
|
|
|
|
constructor (dtp) {
|
|
super(dtp, UserService);
|
|
|
|
this.USER_SELECT = '_id created username username_lc displayName picture flags permissions';
|
|
this.ADMIN_SELECT = '_id created email username username_lc displayName picture flags permissions';
|
|
this.populateUser = [
|
|
{
|
|
path: 'picture.large',
|
|
},
|
|
{
|
|
path: 'picture.small',
|
|
},
|
|
];
|
|
}
|
|
|
|
async start ( ) {
|
|
await super.start();
|
|
|
|
this.registerPassportLocal();
|
|
if (process.env.DTP_ADMIN === 'enabled') {
|
|
this.registerPassportAdmin();
|
|
}
|
|
|
|
this.reservedNames = (await import(path.join(this.dtp.config.root, 'config', 'reserved-names.js'))).default;
|
|
}
|
|
|
|
async create (userDefinition) {
|
|
const NOW = new Date();
|
|
const {
|
|
crypto: cryptoService,
|
|
email: emailService,
|
|
text: textService,
|
|
} = this.dtp.services;
|
|
|
|
try {
|
|
userDefinition.email = userDefinition.email.trim().toLowerCase();
|
|
|
|
// strip characters we don't want to allow in username
|
|
userDefinition.username = await this.filterUsername(userDefinition.username);
|
|
const username_lc = userDefinition.username.toLowerCase();
|
|
|
|
// test the email address for validity, blacklisting, etc.
|
|
await emailService.checkEmailAddress(userDefinition.email);
|
|
|
|
// test if we already have a user with this email address
|
|
let user = await User.findOne({
|
|
$or: [
|
|
{ 'email': userDefinition.email },
|
|
{ username_lc },
|
|
],
|
|
}).lean();
|
|
if (user) {
|
|
throw new SiteError(400, 'That account is not available for use');
|
|
}
|
|
|
|
user = new User();
|
|
user.created = NOW;
|
|
|
|
user.email = userDefinition.email;
|
|
user.username = userDefinition.username;
|
|
user.username_lc = username_lc;
|
|
user.displayName = textService.clean(userDefinition.displayName || userDefinition.username);
|
|
|
|
user.passwordSalt = uuidv4();
|
|
user.password = cryptoService.maskPassword(user.passwordSalt, userDefinition.password);
|
|
|
|
this.log.debug('creating new user account', { user: user.toObject() });
|
|
this.log.info('creating new user account', { email: userDefinition.email });
|
|
await user.save();
|
|
|
|
await emailService.sendWelcomeEmail(user);
|
|
|
|
return user.toObject();
|
|
} catch (error) {
|
|
this.log.error('failed to create user', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async filterUsername (username, options) {
|
|
const { text: textService } = this.dtp.services;
|
|
|
|
options = Object.assign({ checkReserved: true }, options);
|
|
if (!username || (username.length < 1)) {
|
|
throw new SiteError(400, 'Username must not be blank');
|
|
}
|
|
|
|
username = textService.clean(username);
|
|
username = username.replace(/[^A-Za-z0-9\-_]/gi, '');
|
|
|
|
if (options.checkReserved && (await this.isUsernameReserved(username))) {
|
|
throw new SiteError(403, 'The username you entered is not available for use.');
|
|
}
|
|
|
|
return username;
|
|
}
|
|
|
|
async lookup (account, options) {
|
|
options = Object.assign({
|
|
withEmail: false,
|
|
withCredentials: false,
|
|
}, options);
|
|
|
|
this.log.debug('locating user record', { account });
|
|
|
|
const selects = [
|
|
'_id', 'created',
|
|
'username', 'username_lc',
|
|
'displayName', 'picture',
|
|
'flags', 'permissions',
|
|
];
|
|
if (options.withEmail) {
|
|
selects.push('email');
|
|
}
|
|
if (options.withCredentials) {
|
|
selects.push('passwordSalt');
|
|
selects.push('password');
|
|
}
|
|
|
|
const usernameRegex = new RegExp(`^${account}.*`);
|
|
const user = await User
|
|
.findOne({
|
|
$or: [
|
|
{ email: account.email },
|
|
{ username_lc: usernameRegex },
|
|
]
|
|
})
|
|
.select(selects.join(' '))
|
|
.lean();
|
|
|
|
return user;
|
|
}
|
|
|
|
async autocomplete (pagination, username) {
|
|
if (!username || (username.length === 0)) {
|
|
throw new SiteError(406, "Username must not be empty");
|
|
}
|
|
this.log.debug('autocompleting username partial', { username });
|
|
let search = {
|
|
username_lc: new RegExp(username.toLowerCase().trim(), 'gi'),
|
|
};
|
|
const users = await User
|
|
.find(search)
|
|
.sort({ username_lc: 1 })
|
|
.select({ username: 1, username_lc: 1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.lean()
|
|
;
|
|
return users.map((u) => {
|
|
return {
|
|
username: u.username,
|
|
username_lc: u.username_lc,
|
|
};
|
|
});
|
|
}
|
|
|
|
async getByUsername (username, options) {
|
|
options = options || { };
|
|
username = username.trim().toLowerCase();
|
|
|
|
let select = ['+flags', '+permissions'];
|
|
const user = await User
|
|
.findOne({ username_lc: username })
|
|
.select(select.join(' '))
|
|
.populate(this.populateUser)
|
|
.lean();
|
|
|
|
return user;
|
|
}
|
|
|
|
async getUserAccount (userId) {
|
|
const user = await User
|
|
.findById(userId)
|
|
.select('+email +flags +flags.isCloaked +permissions +optIn +paymentTokens.handcash +paymentTokens.stripe')
|
|
.populate([
|
|
{
|
|
path: 'picture.large',
|
|
},
|
|
{
|
|
path: 'picture.small',
|
|
},
|
|
{
|
|
path: 'membership',
|
|
populate: [
|
|
{
|
|
path: 'plan',
|
|
},
|
|
],
|
|
},
|
|
])
|
|
.lean();
|
|
|
|
if (!user) {
|
|
throw new SiteError(404, 'Member account not found');
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
async getUserAccounts (pagination, username) {
|
|
let search = { };
|
|
if (username) {
|
|
search.$or = [
|
|
{ $text: { $search: username } },
|
|
{ username_lc: new RegExp(username.toLowerCase().trim(), 'gi') },
|
|
];
|
|
}
|
|
|
|
const users = await User
|
|
.find(search)
|
|
.sort({ username_lc: 1 })
|
|
.select('+email +flags +permissions, +optIn')
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.lean()
|
|
;
|
|
return users;
|
|
}
|
|
|
|
async getLatestSignups (count = 5) {
|
|
const users = await User
|
|
.find()
|
|
.sort({ created: -1})
|
|
.select(this.ADMIN_SELECT)
|
|
.limit(count)
|
|
.populate(this.populateUser)
|
|
.lean();
|
|
return users;
|
|
}
|
|
|
|
async isUsernameReserved (username) {
|
|
if (this.reservedNames.includes(username)) {
|
|
this.log.alert('prohibiting use of reserved username', { username });
|
|
return true;
|
|
}
|
|
|
|
const user = await User.findOne({ username: username}).select('username').lean();
|
|
if (user) {
|
|
this.log.alert('username is already registered', { username });
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async setEmailVerification (user, isVerified) {
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: { 'flags.isEmailVerified': isVerified },
|
|
},
|
|
);
|
|
}
|
|
|
|
async emailOptOut (userId, category) {
|
|
if (!mongoose.Types.ObjectId.isValid(userId)) {
|
|
throw new SiteError(400, 'Invalid user ID');
|
|
}
|
|
|
|
const user = await this.getUserAccount(userId);
|
|
if (!user) {
|
|
throw new SiteError(406, 'Invalid opt-out token');
|
|
}
|
|
|
|
const updateOp = { $set: { } };
|
|
switch (category) {
|
|
case 'marketing':
|
|
updateOp.$set['optIn.marketing'] = false;
|
|
break;
|
|
case 'system':
|
|
updateOp.$set['optIn.system'] = false;
|
|
break;
|
|
default:
|
|
throw new SiteError(406, 'Invalid opt-out category');
|
|
}
|
|
|
|
await User.updateOne({ _id: userId }, updateOp);
|
|
}
|
|
|
|
async requestPasswordReset (usernameOrEmail) {
|
|
const { authToken: authTokenService, email: emailService } = this.dtp.services;
|
|
const { site } = this.dtp.config;
|
|
|
|
const lookupAccount = {
|
|
email: usernameOrEmail.trim().toLowerCase(),
|
|
username: (await this.filterUsername(usernameOrEmail)).toLowerCase(),
|
|
};
|
|
const recipient = await this.lookup(lookupAccount, { withEmail: true });
|
|
if (!recipient) {
|
|
throw new SiteError(404, 'User account not found');
|
|
}
|
|
|
|
/*
|
|
* Generate a password reset email and send it to the user. They will have
|
|
* to click a link in the email with a token on it that brings them to the
|
|
* password reset view to enter a new password with a fresh reminder to
|
|
* write it down and store that in a safe place.
|
|
*/
|
|
|
|
const passwordResetToken = await authTokenService.create('password-reset', {
|
|
_id: recipient._id,
|
|
username: recipient.username,
|
|
});
|
|
|
|
const templateModel = {
|
|
site,
|
|
messageType: 'system',
|
|
recipient,
|
|
passwordResetToken,
|
|
};
|
|
const message = {
|
|
from: process.env.DTP_EMAIL_SMTP_FROM,
|
|
to: recipient.email,
|
|
subject: `Password reset request for ${recipient.username_lc}@${this.dtp.config.site.domain}!`,
|
|
html: await emailService.renderTemplate('passwordReset', 'html', templateModel),
|
|
text: await emailService.renderTemplate('passwordReset', 'text', templateModel),
|
|
};
|
|
|
|
this.log.info('sending password-reset email', {
|
|
site: site.domain,
|
|
recipient: recipient.username,
|
|
});
|
|
await emailService.send(message);
|
|
|
|
return recipient;
|
|
}
|
|
|
|
async hasPaidMembership (user) {
|
|
if (!user || !user.membership) {
|
|
return false;
|
|
}
|
|
return user.membership.tier !== 'free';
|
|
}
|
|
|
|
registerPassportLocal ( ) {
|
|
const options = {
|
|
usernameField: 'username',
|
|
passwordField: 'password',
|
|
session: true,
|
|
};
|
|
passport.use('dtp-local', new PassportLocal(options, this.handleLocalLogin.bind(this)));
|
|
}
|
|
|
|
async handleLocalLogin (username, password, done) {
|
|
const now = new Date();
|
|
this.log.info('handleLocalLogin', { username });
|
|
try {
|
|
if (!username || (username.length < 1)) {
|
|
throw new SiteError(403, 'Username is required');
|
|
}
|
|
const user = await this.authenticate({ username, password }, { adminRequired: false });
|
|
await this.startSession(user, now);
|
|
done(null, this.filterUserObject(user));
|
|
} catch (error) {
|
|
this.log.error('failed to process local user login', { error });
|
|
done(error);
|
|
}
|
|
}
|
|
|
|
registerPassportAdmin ( ) {
|
|
const options = {
|
|
usernameField: 'username',
|
|
passwordField: 'password',
|
|
session: true,
|
|
};
|
|
this.log.info('registering PassportJS admin strategy', { options });
|
|
passport.use('dtp-admin', new PassportLocal(options, this.handleAdminLogin.bind(this)));
|
|
}
|
|
|
|
async handleAdminLogin (email, password, done) {
|
|
const now = new Date();
|
|
try {
|
|
const user = await this.authenticate({ email, password }, { adminRequired: true });
|
|
await this.startSession(user, now);
|
|
done(null, this.filterUserObject(user));
|
|
} catch (error) {
|
|
this.log.error('failed to process admin user login', { error });
|
|
done(error);
|
|
}
|
|
}
|
|
|
|
async login (req, res, next, options) {
|
|
const { actionAudit: actionAuditService } = this.dtp.services;
|
|
|
|
options = Object.assign({
|
|
loginUrl: '/welcome/login',
|
|
}, options);
|
|
try {
|
|
passport.authenticate('dtp-local', (error, user/*, info*/) => {
|
|
if (error) {
|
|
req.session.loginResult = error.toString();
|
|
return next(error);
|
|
}
|
|
if (!user) {
|
|
req.session.loginResult = 'Username or email address is unknown.';
|
|
if (options.onLoginFailed) {
|
|
return options.onLoginFailed(req, res, next);
|
|
}
|
|
return res.redirect(options.loginUrl);
|
|
}
|
|
|
|
this.log.alert('user login', {
|
|
user: {
|
|
_id: user._id,
|
|
username: user.username,
|
|
ip: req.ip,
|
|
}
|
|
});
|
|
req.login(user, async (error) => {
|
|
if (error) {
|
|
return next(error);
|
|
}
|
|
await actionAuditService.auditRequest(req, 'User login');
|
|
if (options.onLoginSuccess) {
|
|
return options.onLoginSuccess(req, res, next);
|
|
}
|
|
if (!options.redirectUrl) {
|
|
return;
|
|
}
|
|
return res.redirect(options.redirectUrl);
|
|
});
|
|
})(req, res, next);
|
|
} catch (error) {
|
|
this.log.error('failed to process user login', { error });
|
|
return next(error);
|
|
}
|
|
}
|
|
|
|
async updatePhoto (user, file) {
|
|
const { image: imageService } = this.dtp.services;
|
|
const images = [
|
|
{
|
|
width: 512,
|
|
height: 512,
|
|
format: 'jpeg',
|
|
formatParameters: {
|
|
quality: 80,
|
|
},
|
|
},
|
|
{
|
|
width: 64,
|
|
height: 64,
|
|
format: 'jpeg',
|
|
formatParameters: {
|
|
conpressionLevel: 9,
|
|
},
|
|
},
|
|
];
|
|
await imageService.processImageFile(user, file, images);
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: {
|
|
'picture.large': images[0].image._id,
|
|
'picture.small': images[1].image._id,
|
|
},
|
|
},
|
|
);
|
|
return images[0].image;
|
|
}
|
|
|
|
async removePhoto (user) {
|
|
const { image: imageService } = this.dtp.services;
|
|
|
|
this.log.info('remove profile photo', { user: user._id });
|
|
user = await this.getUserAccount(user._id);
|
|
if (user.picture) {
|
|
if (user.picture.large) {
|
|
await imageService.deleteImage(user.picture.large);
|
|
}
|
|
if (user.picture.small) {
|
|
await imageService.deleteImage(user.picture.small);
|
|
}
|
|
}
|
|
await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } });
|
|
}
|
|
|
|
async authenticate (account, options) {
|
|
const { crypto } = this.dtp.services;
|
|
|
|
options = Object.assign({
|
|
adminRequired: false,
|
|
}, options);
|
|
|
|
const user = await User
|
|
.findOne({
|
|
$or: [
|
|
{ email: account.username.trim().toLowerCase() },
|
|
{ username_lc: (await this.filterUsername(account.username, { checkReserved: false })).toLowerCase() },
|
|
]
|
|
})
|
|
.select('_id created username username_lc displayName picture flags permissions email passwordSalt password')
|
|
.lean();
|
|
if (!user) {
|
|
throw new SiteError(404, 'User account not found');
|
|
}
|
|
if (options.adminRequired && !user.flags.isAdmin) {
|
|
throw new SiteError(403, 'Admin privileges required');
|
|
}
|
|
|
|
const maskedPassword = crypto.maskPassword(
|
|
user.passwordSalt,
|
|
account.password,
|
|
);
|
|
if (maskedPassword !== user.password) {
|
|
throw new SiteError(403, 'Account credentials do not match');
|
|
}
|
|
|
|
// remove these critical fields from the user object
|
|
delete user.passwordSalt;
|
|
delete user.password;
|
|
|
|
this.log.debug('user authenticated', { username: user.username });
|
|
|
|
return user;
|
|
}
|
|
|
|
async startSession (user, now) {
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: { 'stats.lastLogin': now },
|
|
$inc: { 'stats.loginCount': 1 },
|
|
},
|
|
);
|
|
}
|
|
|
|
async updatePassword (user, password) {
|
|
const { crypto: cryptoService } = this.dtp.services;
|
|
|
|
const passwordSalt = uuidv4();
|
|
const passwordHash = cryptoService.maskPassword(passwordSalt, password);
|
|
|
|
this.log.info('updating user password', { userId: user._id });
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: {
|
|
passwordSalt: passwordSalt,
|
|
password: passwordHash,
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
async setUserSettings (user, settings) {
|
|
const {
|
|
crypto: cryptoService,
|
|
email: emailService,
|
|
text: textService,
|
|
} = this.dtp.services;
|
|
|
|
const update = { $set: { } };
|
|
const actions = [ ];
|
|
let emailChanged = false;
|
|
|
|
if (settings.displayName && (settings.displayName !== user.displayName)) {
|
|
update.$set.displayName = textService.clean(settings.displayName).trim();
|
|
actions.push('Display name updated');
|
|
}
|
|
if (settings.bio && (settings.bio !== user.bio)) {
|
|
update.$set.bio = textService.filter(settings.bio).trim();
|
|
actions.push('bio updated');
|
|
}
|
|
|
|
if (!settings.username) {
|
|
throw new SiteError(400, 'Username must not be empty');
|
|
}
|
|
if (settings.username && (settings.username !== user.username)) {
|
|
update.$set.username = await this.filterUsername(settings.username);
|
|
update.$set.username_lc = update.$set.username.toLowerCase();
|
|
}
|
|
|
|
if (settings.email && (settings.email !== user.email)) {
|
|
settings.email = settings.email.toLowerCase().trim();
|
|
await emailService.checkEmailAddress(settings.email);
|
|
update.$set['flags.isEmailVerified'] = false;
|
|
update.$set.email = settings.email;
|
|
actions.push('Email address updated and verification email sent. Please check your inbox and follow the instructions included to complete the change of your email address.');
|
|
emailChanged = true;
|
|
}
|
|
|
|
if (settings.password) {
|
|
if (settings.password !== settings.passwordv) {
|
|
throw new SiteError(400, 'Password and password verification do not match.');
|
|
}
|
|
update.$set.passwordSalt = uuidv4();
|
|
update.$set.password = cryptoService.maskPassword(update.$set.passwordSalt, settings.password);
|
|
actions.push('Password changed successfully.');
|
|
}
|
|
|
|
update.$set.optIn = {
|
|
system: settings['optIn.system'] === 'on',
|
|
newsletter: settings['optIn.newsletter'] === 'on',
|
|
};
|
|
|
|
if (user.flags && user.flags.isModerator) {
|
|
update.$set['flags.isCloaked'] = settings['flags.isCloaked'] === 'on';
|
|
}
|
|
|
|
update.$set['ui.theme'] = settings.uiTheme;
|
|
|
|
await User.updateOne({ _id: user._id }, update);
|
|
|
|
/*
|
|
* Re-load the User from the database, and use the updated User to send a
|
|
* new welcome email if the email address was changed.
|
|
*/
|
|
if (emailChanged) {
|
|
user = await this.getUserAccount(user._id);
|
|
await this.sendWelcomeEmail(user);
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
async setLastAnnouncement (user, announcement) {
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: { lastAnnouncement: announcement.created },
|
|
},
|
|
);
|
|
}
|
|
|
|
filterUserObject (user) {
|
|
return {
|
|
_id: user._id,
|
|
created: user.created,
|
|
username: user.username,
|
|
username_lc: user.username_lc,
|
|
displayName: user.displayName,
|
|
bio: user.bio,
|
|
picture: user.picture,
|
|
flags: user.flags,
|
|
permissions: user.permissions,
|
|
membership: user.membership,
|
|
stats: user.stats,
|
|
};
|
|
}
|
|
|
|
async updateForAdmin (user, userDefinition) {
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: {
|
|
'flags.isAdmin': userDefinition['flags.isAdmin'] === 'on',
|
|
'flags.isModerator': userDefinition['flags.isModerator'] === 'on',
|
|
'flags.isEmailVerified': userDefinition['flags.isEmailVerified'] === 'on',
|
|
|
|
'permissions.canLogin': userDefinition['permissions.canLogin'] === 'on',
|
|
'permissions.canReport': userDefinition['permissions.canReport'] === 'on',
|
|
'permissions.canShareLinks': userDefinition['permissions.canShareLinks'] === 'on',
|
|
|
|
'optIn.system': userDefinition['optIn.system'] === 'on',
|
|
'optIn.marketing': userDefinition['optIn.marketing'] === 'on',
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
async banUser (user) {
|
|
const {
|
|
image: imageService,
|
|
link: linkService,
|
|
otpAuth: otpAuthService,
|
|
video: videoService,
|
|
} = this.dtp.services;
|
|
|
|
const userTag = { _id: user._id, username: user.username };
|
|
this.log.alert('banning user', userTag);
|
|
|
|
await otpAuthService.removeForUser(user);
|
|
await linkService.removeForUser(user);
|
|
|
|
await imageService.removeForUser(user);
|
|
await videoService.removeForUser(user);
|
|
|
|
this.log.info('removing all user privileges', userTag);
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: {
|
|
'flags.isAdmin': false,
|
|
'flags.isModerator': false,
|
|
'flags.isEmailVerified': false,
|
|
'permissions.canLogin': false,
|
|
'permissions.canComment': false,
|
|
'permissions.canReport': false,
|
|
'permissions.canShareLinks': false,
|
|
'optIn.system': false,
|
|
'optIn.marketing': false,
|
|
badges: [ ],
|
|
favoriteStickers: [ ],
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
async block (user, blockedUserId) {
|
|
blockedUserId = mongoose.Types.ObjectId.createFromHexString(blockedUserId);
|
|
this.log.info('blocking user', { user: user._id, blockedUserId });
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{ $addToSet: { blockedUsers: blockedUserId } },
|
|
);
|
|
}
|
|
|
|
async getBlockList (userId) {
|
|
const user = await User
|
|
.findOne({ _id: userId })
|
|
.select('blockedUsers')
|
|
.lean();
|
|
if (!user) {
|
|
return [ ];
|
|
}
|
|
return user.blockedUsers || [ ];
|
|
}
|
|
|
|
async getBlockedUsers (userId) {
|
|
const user = await User
|
|
.findOne({ _id: userId })
|
|
.select('blockedUsers')
|
|
.populate({
|
|
path: 'blockedUsers',
|
|
select: this.USER_SELECT,
|
|
})
|
|
.lean();
|
|
if (!user) {
|
|
return [ ];
|
|
}
|
|
return user.blockedUsers || [ ];
|
|
}
|
|
|
|
async unblock (user, blockedUserId) {
|
|
await User.updateOne(
|
|
{ _id: user._id },
|
|
{ $pull: { blockedUsers: blockedUserId } },
|
|
);
|
|
}
|
|
|
|
async isBlocked (user, blockedUserId) {
|
|
/*
|
|
* This is faster than using countDocuments - just fetch the _id
|
|
*/
|
|
const test = await User
|
|
.findOne({ _id: user._id, blockedUsers: blockedUserId })
|
|
.select('_id') // ignoring all data, do they exist?
|
|
.lean();
|
|
return !!test;
|
|
}
|
|
}
|