// user.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict' ;
const path = require ( 'path' ) ;
const mongoose = require ( 'mongoose' ) ;
const User = mongoose . model ( 'User' ) ;
const CoreUser = mongoose . model ( 'CoreUser' ) ;
const UserBlock = mongoose . model ( 'UserBlock' ) ;
const UserArchive = mongoose . model ( 'UserArchive' ) ;
const passport = require ( 'passport' ) ;
const PassportLocal = require ( 'passport-local' ) ;
const striptags = require ( 'striptags' ) ;
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 ) {
super ( dtp , module . exports ) ;
this . USER _SELECT = '_id username username_lc displayName picture' ;
this . reservedNames = require ( path . join ( this . dtp . config . root , 'config' , 'reserved-names' ) ) ;
this . populateUser = [
{
path : 'picture.large' ,
} ,
{
path : 'picture.small' ,
} ,
] ;
}
async start ( ) {
await super . start ( ) ;
this . registerPassportLocal ( ) ;
if ( process . env . DTP _ADMIN === 'enabled' ) {
this . registerPassportAdmin ( ) ;
}
const { jobQueue : jobQueueService } = this . dtp . services ;
this . jobQueues = { } ;
this . log . info ( 'connecting to job queue' , { name : 'reeeper' , config : this . dtp . config . jobQueues . reeeper } ) ;
this . jobQueues . reeeper = jobQueueService . getJobQueue (
'reeeper' ,
this . dtp . config . jobQueues . reeeper ,
) ;
}
async stop ( ) {
this . log . info ( ` stopping ${ module . exports . name } service ` ) ;
await super . stop ( ) ;
}
async create ( userDefinition ) {
const NOW = new Date ( ) ;
const {
crypto : cryptoService ,
email : mailService ,
} = this . dtp . services ;
try {
this . checkRestrictedKeys ( 'create' , userDefinition ) ;
userDefinition . email = userDefinition . email . trim ( ) . toLowerCase ( ) ;
// strip characters we don't want to allow in username
userDefinition . username = userDefinition . username . trim ( ) . replace ( /[^A-Za-z0-9\-_]/gi , '' ) ;
const username _lc = userDefinition . username . toLowerCase ( ) ;
await this . checkUsername ( username _lc ) ;
// test the email address for validity, blacklisting, etc.
await mailService . checkEmailAddress ( userDefinition . email ) ;
// test if we already have a user with this email address
let user = await User . findOne ( { 'email' : userDefinition . email . toLowerCase ( ) . trim ( ) } ) . lean ( ) ;
if ( user ) {
throw new SiteError ( 400 , ` An account with email address ${ userDefinition . email } already exists. ` ) ;
}
// test if we already have a user with this username
user = await User . findOne ( { username _lc } ) . lean ( ) ;
if ( user ) {
throw new SiteError ( 400 , ` An account with username ${ userDefinition . username } already exists. ` ) ;
}
const passwordSalt = uuidv4 ( ) ;
const maskedPassword = cryptoService . maskPassword ( passwordSalt , userDefinition . password ) ;
user = new User ( ) ;
user . created = NOW ;
user . email = userDefinition . email ;
user . username = userDefinition . username ;
user . username _lc = username _lc ;
user . displayName = striptags ( userDefinition . displayName || userDefinition . username ) ;
user . passwordSalt = passwordSalt ;
user . password = maskedPassword ;
user . flags = {
isAdmin : false ,
isModerator : false ,
isEmailVerified : false ,
} ;
user . permissions = {
canLogin : true ,
canChat : true ,
canComment : true ,
canReport : true ,
} ;
user . optIn = {
system : true ,
marketing : false ,
} ;
this . log . info ( 'creating new user account' , { email : userDefinition . email } ) ;
await user . save ( ) ;
await this . sendWelcomeEmail ( user ) ;
return user . toObject ( ) ;
} catch ( error ) {
this . log . error ( 'failed to create user' , { error } ) ;
throw error ;
}
}
async sendWelcomeEmail ( user ) {
const { email : emailService } = this . dtp . services ;
/ *
* Remove all pending EmailVerify tokens for the User .
* /
await emailService . removeVerificationTokensForUser ( user ) ;
/ *
* Create the new / o n l y E m a i l V e r i f y t o k e n f o r t h e u s e r . T h i s w i l l b e t h e o n l y
* token accepted . Previous emails sent ( if they were received ) are invalid
* after this .
* /
const verifyToken = await emailService . createVerificationToken ( user ) ;
/ *
* Send the welcome email using the new EmailVerify token so it can
* construct a new , valid link to use for verifying the email address .
* /
const templateModel = {
site : this . dtp . config . site ,
recipient : user ,
emailVerifyToken : verifyToken . token ,
} ;
const message = {
from : process . env . DTP _EMAIL _SMTP _FROM ,
to : user . email ,
subject : ` Welcome to ${ this . dtp . config . site . name } ! ` ,
html : await emailService . renderTemplate ( 'welcome' , 'html' , templateModel ) ,
text : await emailService . renderTemplate ( 'welcome' , 'text' , templateModel ) ,
} ;
await emailService . send ( message ) ;
}
async setEmailVerification ( user , isVerified ) {
await User . updateOne (
{ _id : user . _id } ,
{
$set : { 'flags.isEmailVerified' : isVerified } ,
} ,
) ;
}
async emailOptOut ( userId , category ) {
userId = mongoose . Types . ObjectId ( userId ) ;
const user = await this . getLocalUserAccount ( 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 update ( user , userDefinition ) {
if ( ! user . flags . canLogin ) {
throw SiteError ( 403 , 'Invalid user account operation' ) ;
}
this . checkRestrictedKeys ( 'create' , userDefinition ) ;
userDefinition . username = striptags ( userDefinition . username . trim ( ) . replace ( /[^A-Za-z0-9\-_]/gi , '' ) ) ;
const username _lc = userDefinition . username . toLowerCase ( ) ;
userDefinition . displayName = striptags ( userDefinition . displayName . trim ( ) ) ;
this . log . info ( 'updating user' , { userDefinition } ) ;
await User . updateOne (
{ _id : user . _id } ,
{
$set : {
username : userDefinition . username ,
username _lc ,
displayName : userDefinition . displayName ,
'optIn.system' : userDefinition [ 'optIn.system' ] === 'on' ,
'optIn.marketing' : userDefinition [ 'optIn.marketing' ] === 'on' ,
} ,
} ,
) ;
}
async updateLocalForAdmin ( user , userDefinition ) {
userDefinition . username = striptags ( userDefinition . username . trim ( ) . replace ( /[^A-Za-z0-9\-_]/gi , '' ) ) ;
const username _lc = userDefinition . username . toLowerCase ( ) ;
userDefinition . displayName = striptags ( userDefinition . displayName . trim ( ) ) ;
if ( userDefinition . badges ) {
userDefinition . badges = userDefinition . badges . split ( ',' ) . map ( ( badge ) => striptags ( badge . trim ( ) ) ) ;
} else {
userDefinition . badges = [ ] ;
}
this . log . info ( 'updating user for admin' , { userDefinition } ) ;
await User . updateOne (
{ _id : user . _id } ,
{
$set : {
username : userDefinition . username ,
username _lc ,
displayName : userDefinition . displayName ,
bio : striptags ( userDefinition . bio . trim ( ) ) ,
badges : userDefinition . badges ,
'flags.isAdmin' : userDefinition . isAdmin === 'on' ,
'flags.isModerator' : userDefinition . isModerator === 'on' ,
'flags.isEmailVerified' : userDefinition . isEmailVerified === 'on' ,
'permissions.canLogin' : userDefinition . canLogin === 'on' ,
'permissions.canChat' : userDefinition . canChat === 'on' ,
'permissions.canComment' : userDefinition . canComment === 'on' ,
'permissions.canReport' : userDefinition . canReport === 'on' ,
'optIn.system' : userDefinition . optInSystem === 'on' ,
'optIn.marketing' : userDefinition . optInMarketing === 'on' ,
} ,
} ,
) ;
}
async updateSettings ( user , userDefinition ) {
const { crypto : cryptoService } = this . dtp . services ;
const updateOp = { $set : { } , $unset : { } } ;
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' ) ;
}
updateOp . $set . username _lc = updateOp . $set . username . toLowerCase ( ) ;
if ( userDefinition . displayName && ( userDefinition . displayName . length > 0 ) ) {
updateOp . $set . displayName = striptags ( userDefinition . displayName . trim ( ) ) ;
} else {
updateOp . $unset . displayName = 1 ;
}
if ( userDefinition . bio && ( userDefinition . bio . length > 0 ) ) {
updateOp . $set . bio = striptags ( userDefinition . bio . trim ( ) ) ;
} else {
updateOp . $unset . bio = 1 ;
}
if ( userDefinition . password && userDefinition . password . length > 0 ) {
updateOp . $set . passwordSalt = uuidv4 ( ) ;
updateOp . $set . password = cryptoService . maskPassword ( updateOp . $set . passwordSalt , userDefinition . password ) ;
}
updateOp . $set . theme = userDefinition . theme || 'dtp-light' ;
this . log . info ( 'updating user settings' , { userId : user . _id } ) ;
await User . updateOne ( { _id : user . _id } , updateOp ) ;
}
async authenticate ( account , options ) {
const { crypto } = this . dtp . services ;
options = Object . assign ( {
adminRequired : false ,
} , options ) ;
const accountEmail = account . username . trim ( ) . toLowerCase ( ) ;
const accountUsername = this . filterUsername ( accountEmail ) ;
this . log . debug ( 'locating user record' , { accountEmail , accountUsername } ) ;
const user = await User
. findOne ( {
$or : [
{ email : accountEmail } ,
{ username _lc : accountUsername } ,
]
} )
. select ( '+passwordSalt +password +flags +optIn +permissions' )
. lean ( ) ;
if ( ! user ) {
throw new SiteError ( 404 , 'Member credentials are invalid' ) ;
}
const maskedPassword = crypto . maskPassword (
user . passwordSalt ,
account . password ,
) ;
if ( maskedPassword !== user . password ) {
throw new SiteError ( 403 , 'Member credentials are invalid' ) ;
}
// remove these critical fields from the user object
delete user . passwordSalt ;
delete user . password ;
if ( options . adminRequired && ! user . flags . isAdmin ) {
throw new SiteError ( 403 , 'Admin privileges required' ) ;
}
return user ;
}
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 {
const user = await this . authenticate ( { username , password } , { adminRequired : false } ) ;
await this . startUserSession ( 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 . startUserSession ( user , now ) ;
done ( null , this . filterUserObject ( user ) ) ;
} catch ( error ) {
this . log . error ( 'failed to process admin user login' , { error } ) ;
done ( error ) ;
}
}
async startUserSession ( user , now ) {
user . type = 'User' ;
await User . updateOne (
{ _id : user . _id } ,
{
$set : { 'stats.lastLogin' : now } ,
$inc : { 'stats.loginCount' : 1 } ,
} ,
) ;
}
async getLocalUserId ( username ) {
const user = await User . findOne ( { username _lc : username } ) . select ( '_id' ) . lean ( ) ;
if ( ! user ) {
return ; // undefined
}
return user . _id ;
}
async getCoreUserId ( username ) {
const user = await CoreUser . findOne ( { username _lc : username } ) . select ( '_id' ) . lean ( ) ;
if ( ! user ) {
return ; // undefined
}
return user . _id ;
}
async getLocalUserAccount ( userId ) {
const user = await User
. findById ( userId )
. select ( '+email +flags +permissions +optIn +picture' )
. populate ( this . populateUser )
. lean ( ) ;
if ( ! user ) {
throw new SiteError ( 404 , 'Member account not found' ) ;
}
user . type = 'User' ;
return user ;
}
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 } )
. select ( '+email +flags +permissions +optIn' )
. skip ( pagination . skip )
. limit ( pagination . cpp )
. lean ( )
;
return users . map ( ( user ) => { user . type = 'User' ; return user ; } ) ;
}
async searchCoreUserAccounts ( pagination , username ) {
let search = { } ;
username = this . filterUsername ( username ) ;
if ( username ) {
search . username _lc = { $regex : ` ^ ${ username . toLowerCase ( ) . trim ( ) } ` } ;
}
const users = await CoreUser
. find ( search )
. sort ( { username _lc : 1 } )
. select ( '+core +coreUserId +flags +permissions +optIn' )
. skip ( pagination . skip )
. limit ( pagination . cpp )
. lean ( )
;
return users . map ( ( user ) => { user . type = 'CoreUser' ; return user ; } ) ;
}
async getRecent ( maxCount = 3 ) {
const users = User
. find ( )
. select ( UserService . USER _SELECT )
. sort ( { created : - 1 } )
. limit ( maxCount )
. lean ( ) ;
return users ;
}
async getAdmins ( ) {
const admins = await User
. find ( { 'flags.isAdmin' : true } )
. select ( UserService . USER _SELECT )
. sort ( { username : 1 } )
. lean ( ) ;
return admins ;
}
async getModerators ( ) {
const moderators = await User
. find ( { 'flags.isModerator' : true } )
. select ( UserService . USER _SELECT )
. sort ( { username : 1 } )
. lean ( ) ;
return moderators ;
}
async setUserSettings ( user , settings ) {
const {
crypto : cryptoService ,
mail : mailService ,
phone : phoneService ,
} = this . dtp . platform . services ;
const update = { $set : { } } ;
const actions = [ ] ;
if ( settings . name && ( settings . name !== user . name ) ) {
update . name = striptags ( settings . name . trim ( ) ) ;
update . name _lc = update . name . toLowerCase ( ) ;
actions . push ( 'Display name updated' ) ;
}
if ( settings . username && ( settings . username !== user . username ) ) {
update . username = this . filterUsername ( settings . username ) ;
await this . checkUsername ( update . username ) ;
}
if ( settings . email && ( settings . email !== user . email ) ) {
settings . email = settings . email . toLowerCase ( ) . trim ( ) ;
await mailService . 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.' ) ;
}
/ *
* User is changing the phone number stored on the account .
* "There's a lot to unpack here"
* /
if ( settings . phone ) {
// update the phone number (there's a lot going on here)
try {
update . $set . phone = await phoneService . processPhoneNumberInput ( settings . phone ) ;
} catch ( error ) {
throw error ;
}
// un-verify the account's phone number
update . $set [ 'flags.isPhoneVerified' ] = false ;
actions . push ( 'Phone number updated and verification message sent. Please follow the instructions in the text message to complete the change of your mobile phone number.' ) ;
}
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.' ) ;
}
if ( settings . theme ) {
update . $set [ 'settings.theme' ] = striptags ( settings . theme . trim ( ) ) ;
}
if ( settings . language ) {
update . $set [ 'settings.language' ] = mongoose . Types . ObjectId ( settings . language ) ;
actions . push ( 'Interface language changed.' ) ;
}
await User . updateOne ( { _id : user . _id } , update ) ;
return actions ;
}
async checkUsername ( username ) {
if ( ! username || ( typeof username !== 'string' ) || ( username . length === 0 ) ) {
throw new SiteError ( 406 , 'Invalid username' ) ;
}
if ( this . reservedNames . includes ( username . trim ( ) . toLowerCase ( ) ) ) {
throw new SiteError ( 403 , 'That username is reserved for system use' ) ;
}
const user = await User . findOne ( { username : username } ) . select ( 'username' ) . lean ( ) ;
if ( user ) {
this . log . alert ( 'username is already registered' , { username } ) ;
throw new SiteError ( 403 , 'Username is already registered' ) ;
}
}
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 ) ;
}
async getTotalCount ( ) {
return await User . estimatedDocumentCount ( ) ;
}
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 : {
compressionLevel : 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 ,
} ,
} ,
) ;
}
async removePhoto ( user ) {
const { image : imageService } = this . dtp . services ;
this . log . info ( 'remove profile photo' , { user : user . _id } ) ;
switch ( user . type ) {
case 'User' :
user = await this . getLocalUserAccount ( user . _id ) ;
break ;
case 'CoreUser' :
user = await this . getCoreUserAccount ( user . _id ) ;
break ;
default :
throw new SiteError ( 400 , 'Invalid User type' ) ;
}
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 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" ) ;
}
await UserBlock . updateOne (
{ 'member.user' : user . _id } ,
{
$addToSet : {
blockedMembers : {
userType : blockedUser . type ,
user : blockedUser . _id ,
} ,
} ,
} ,
{ upsert : true } ,
) ;
}
async unblockUser ( user , blockedUser ) {
if ( user . _id . equals ( blockedUser . _id ) ) {
throw new SiteError ( 406 , "You can't un-block yourself" ) ;
}
await UserBlock . updateOne (
{ 'member.user' : user . _id } ,
{
$removeFromSet : {
blockedUsers : {
userType : blockedUser . type ,
user : blockedUser . _id ,
} ,
} ,
} ,
) ;
}
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 ,
}
}
) ;
}
/ * *
* Updates the ` lastAnnouncement ` field of a User to the ` created ` date of the
* specified announcement ( for tracking last - seen announcements ) .
* @ param { User } user The user being updated
* @ param { Announcement } announcement The announcement being seen by the User
* /
async setLastAnnouncement ( user , announcement ) {
await User . updateOne (
{ _id : user . _id } ,
{
$set : { lastAnnouncement : announcement . created } ,
} ,
) ;
}
async ban ( user ) {
const {
chat : chatService ,
comment : commentService ,
contentReport : contentReportService ,
csrfToken : csrfTokenService ,
otpAuth : otpAuthService ,
sticker : stickerService ,
userNotification : userNotificationService ,
} = this . dtp . services ;
const userModel = mongoose . model ( user . type ) ;
await userModel . updateOne (
{ _id : user . _id } ,
{
$set : {
'flags.isAdmin' : false ,
'flags.isModerator' : false ,
'flags.isEmailVerified' : false ,
'permissions.canLogin' : false ,
'permissions.canChat' : false ,
'permissions.canComment' : false ,
'permissions.canReport' : false ,
'optIn.system' : false ,
'optIn.marketing' : false ,
} ,
} ,
) ;
await chatService . removeForUser ( user ) ;
await commentService . removeForAuthor ( user ) ;
await contentReportService . removeForUser ( user ) ;
await csrfTokenService . removeForUser ( user ) ;
await otpAuthService . removeForUser ( user ) ;
await stickerService . removeForUser ( user ) ;
await userNotificationService . removeForUser ( user ) ;
}
checkRestrictedKeys ( method , definition ) {
const { logan : loganService } = this . dtp . services ;
const restrictedKeys = [
'isAdmin' , 'isModerator' , 'isEmailVerified' ,
'canLogin' , 'canChat' , 'canComment' , 'canReport' ,
'optInSystem' , 'optInMarketing' ,
] ;
const keys = Object . keys ( definition ) ;
for ( const restrictedKey of restrictedKeys ) {
if ( keys . includes ( restrictedKey ) ) {
loganService . sendEvent ( module . exports , {
level : 'alert' ,
event : method ,
message : 'malicious fields detected' ,
data : { definition } ,
} ) ;
throw new SiteError ( 403 , 'invalid request' ) ;
}
}
}
/ * *
* Create a job to archive and ban a User ( local ) . The job will immediately
* disable the specified user , create a . zip file of their content on storage .
* Once the worker confirms that the archive file is on storage , it creates a
* UserArchive record for it , then completely bans the User . That removes all
* of the User ' s content .
*
* It then removes the User record entirely .
*
* @ param { User } user the User to be archived
* @ returns the newly created Bull queue job
* /
async archiveLocalUser ( user ) {
return this . jobQueues . reeeper . add ( 'archive-user-local' , { userId : user . _id } ) ;
}
/ * *
* Update a UserArchive document
* @ param { UserArchive } archive the existing archive to be updated
* @ param { * } archiveDefinition new values to be applied
* /
async updateArchive ( archive , archiveDefinition ) {
const update = { $set : { } , $unset : { } } ;
archiveDefinition . notes = archiveDefinition . notes . trim ( ) ;
if ( archiveDefinition . notes && ( archiveDefinition . notes . length > 0 ) ) {
update . $set . notes = archiveDefinition . notes ;
} else {
update . $unset . notes = 1 ;
}
await UserArchive . updateOne ( { _id : archive . _id } , update ) ;
}
/ * *
* Fetch an Array of UserArchive documents with pagination .
* @ param { DtpPagination } pagination self explanatory
* @ returns Array of UserArchive documents ( can be empty )
* /
async getArchives ( pagination ) {
const search = { } ;
const archives = await UserArchive
. find ( search )
. sort ( { created : - 1 } )
. skip ( pagination . skip )
. limit ( pagination . cpp )
. lean ( ) ;
const totalArchiveCount = await UserArchive . estimatedDocumentCount ( ) ;
return { archives , totalArchiveCount } ;
}
/ * *
* Fetch a UserArchive record . This does not fetch the archive file .
* @ param { UserArchive } archiveId the ID of the archive to fetch
* @ returns the requested UserArchive , or null / undefined .
* /
async getArchiveById ( archiveId ) {
const archive = await UserArchive . findOne ( { _id : archiveId } ) . lean ( ) ;
return archive ;
}
/ * *
* Removes the . zip file attached to a UserArchive .
* @ param { UserArchive } archive the archive for which an associated . zip file
* is to be removed
* /
async deleteArchiveFile ( archive ) {
const { minio : minioService } = this . dtp . services ;
if ( ! archive . archive || ! archive . archive . bucket || ! archive . archive . key ) {
return ; // no archive file present, abort
}
await minioService . removeObject ( archive . archive . bucket , archive . archive . key ) ;
await UserArchive . updateOne (
{ _id : archive . _id } ,
{
$unset : { archive : 1 } ,
} ,
) ;
}
/ * *
* Removes a UserArchive and any attached data .
* @ param { UserArchive } archive the UserArchive to be removed .
* /
async deleteArchive ( archive ) {
await this . deleteArchiveFile ( archive ) ;
await UserArchive . deleteOne ( { _id : archive . _id } ) ;
}
}
module . exports = {
slug : 'user' ,
name : 'user' ,
className : 'UserService' ,
create : ( dtp ) => { return new UserService ( dtp ) ; } ,
} ;