Steve Smith
/* eslint-disable consistent-return */
const _ = require('lodash');
const passport = require('passport');
const Joi = require('joi');
const fs = require('fs');
const path = require('path');
const Image = require('../../media/components/image');
const SYSTEM_CONST = require('../../system/constants');
/**
* Create a new user
* Using by Administrator
*/
exports.create = async (req, res, next) => {
try {
const schema = Joi.object().keys({
username: Joi.string().required(),
phoneNumber: Joi.string().optional(),
role: Joi.string().valid(['admin', 'user']).default('user').required(),
type: Joi.string().valid(['user', 'model']).default('user').required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
availableToken: Joi.number().min(0).allow([null]).optional(),
address: Joi.string().allow([null, '']).optional(),
state: Joi.string().allow([null, '']).optional(),
city: Joi.string().allow([null, '']).optional(),
country: Joi.string().allow([null, '']).optional(),
isActive: Joi.boolean().allow([null]).default(true).optional(),
avatar: Joi.string().allow([null, '']).optional(),
emailVerified: Joi.boolean().allow([null]).default(true).optional(),
isCompletedProfile: Joi.boolean().allow([null]).default(true).optional(),
isBlocked: Joi.boolean().allow([null]).default(true).optional()
});
const validate = Joi.validate(req.body, schema);
if (validate.error) {
return next(PopulateResponse.validationError(validate.error));
}
if (validate.value.role === 'admin' && !validate.value.password) {
return next(
PopulateResponse.validationError({ msg: 'Admin accounnt needs password to login, please enter password!' })
);
}
const user = await Service.User.create(validate.value);
res.locals.user = user;
return next();
} catch (e) {
return next(e);
}
};
/**
* do update for admin update
*/
exports.update = async (req, res, next) => {
try {
const user = req.params.id ? await DB.User.findOne({ _id: req.params.id }) : req.user;
let publicFields = ['address', 'phoneNumber', 'isActive', 'avatar', 'email', 'isBlocked'];
if (req.user.role === 'admin') {
publicFields = publicFields.concat(['isCompletedProfile', 'emailVerified', 'role', 'username', 'availableToken']);
req.body.password && (publicFields = publicFields.concat(['password']));
}
const fields = _.pick(req.body, publicFields);
_.merge(user, fields);
if (user.type === 'user') {
user.isApproved = true;
}
await user.save();
res.locals.update = user;
next();
} catch (e) {
next(e);
}
};
exports.me = (req, res, next) => {
res.locals.me = req.user.getPublicProfile();
next();
};
exports.findOne = async (req, res, next) => {
try {
const user = await DB.User.findOne({
_id: req.params.id
});
res.locals.user = user;
next();
} catch (e) {
next(e);
}
};
/**
* find user from Add Contact Page
*/
exports.findByUsername = async (req, res, next) => {
try {
const schema = Joi.object().keys({
username: Joi.string().required()
});
const validate = Joi.validate(req.params, schema);
if (validate.error) {
return next(PopulateResponse.validationError(validate.error));
}
const query = _.merge(validate.value, {
type: { $ne: req.user.type },
isCompletedProfile: true,
isApproved: true,
isActive: true,
isBlocked: false
});
const user = await DB.User.findOne(query);
if (!user) {
return next(PopulateResponse.notFound({ message: 'User is not found' }));
}
const contact = await DB.Contact.findOne({
$or: [
{ addedBy: req.user._id, userId: user._id },
{ addedBy: user._id, userId: req.user._id }
]
});
res.locals.user = { ...user.getPublicProfile(), isFriend: contact ? true : false, contactId: contact?._id || null };
return next();
} catch (e) {
return next(e);
}
};
/**
* update user avatar
*/
exports.updateAvatar = async (req, res, next) => {
try {
const user = req.params.id ? await DB.User.findOne({ _id: req.params.id }) : req.user;
if (!user) {
return next(PopulateResponse.notFound());
}
const avatarSize = await DB.Config.findOne({ key: SYSTEM_CONST.AVATAR_SIZE });
if (!avatarSize || !avatarSize.value || !avatarSize.value.width || !avatarSize.value.height) {
return PopulateResponse.serverError({ msg: 'Missing avatar size!' });
}
// create thumb for the avatar
const thumbPath = await Image.resize({
input: req.file.path,
width: avatarSize.value.width || 250,
height: avatarSize.value.height || 250,
resizeOption: '^'
});
await DB.User.update({ _id: req.params.id || req.user._id }, { $set: { avatar: thumbPath } });
// unlink old avatar
if (user.avatar && !Helper.String.isUrl(user.avatar) && fs.existsSync(path.resolve(user.avatar))) {
fs.unlinkSync(path.resolve(user.avatar));
}
// remove tmp file
// if (fs.existsSync(path.resolve(req.file.path))) {
// fs.unlinkSync(path.resolve(req.file.path));
// }
res.locals.updateAvatar = {
url: DB.User.getAvatarUrl(thumbPath)
};
return next();
} catch (e) {
return next(e);
}
};
exports.search = async (req, res, next) => {
const page = Math.max(0, req.query.page - 1) || 0; // using a zero-based page index for use with skip()
const take = parseInt(req.query.take, 10) || 10;
try {
let query = Helper.App.populateDbQuery(req.query, {
text: ['phoneNumber', 'email', 'username'],
boolean: ['isOnline', 'isApproved', 'isCompletedProfile', 'isCompletedDocument', 'isActive', 'isBlocked'],
equal: ['role', 'type', 'gender', 'city', 'state', 'country']
});
if (req.user.role !== 'admin') {
query = {
...query,
isApproved: true,
isCompletedProfile: true,
isActive: true,
isBlocked: false
};
}
const sort = Helper.App.populateDBSort(req.query);
const count = await DB.User.count(query);
const items = await DB.User.find(query)
.collation({ locale: 'en' })
.sort(sort)
.skip(page * take)
.limit(take)
.exec();
res.locals.search = {
count,
items: await checkAndConvertFriend(items, req.user)
};
next();
} catch (e) {
next(e);
}
};
exports.searchFriends = async (req, res, next) => {
const page = Math.max(0, req.query.page - 1) || 0; // using a zero-based page index for use with skip()
const take = parseInt(req.query.take, 10) || 10;
try {
let query = Helper.App.populateDbQuery(req.query, {
text: ['phoneNumber', 'email', 'username'],
boolean: ['isOnline', 'isApproved', 'isCompletedProfile', 'isCompletedDocument', 'isActive', 'isBlocked'],
equal: ['role', 'type', 'gender', 'city', 'state', 'country']
});
if (req.user.role !== 'admin') {
query = {
...query,
isApproved: true,
isCompletedProfile: true,
isActive: true,
isBlocked: false
};
}
const sort = Helper.App.populateDBSort(req.query);
const items = await DB.User.find(query)
.collation({ locale: 'en' })
.sort(sort)
.exec();
// add user response and check for friend
const newItems = await checkAndConvertFriend(items, req.user);
const data = newItems.filter((i) => i.isFriend);
res.locals.search = {
count: data.length,
items: data.slice(page * take, (page + 1) * take )
};
next();
} catch (e) {
next(e);
}
};
async function checkAndConvertFriend(models, user) {
const query = {
$or: [
{ userId: user._id, addedBy: { $in: models } },
{ userId: { $in: models }, addedBy: user._id }
]
};
const contacts = await DB.Contact.find(query);
const array = models.map(model => {
let data = user?.role === 'admin' ? model : model.getPublicProfile();
const isFriend = contacts.find(
contact =>
contact.userId.toString() === model._id.toString() || contact.addedBy.toString() === model._id.toString()
);
data.isFriend = isFriend ? true : false;
return data;
});
return array;
}
exports.remove = async (req, res, next) => {
try {
const user = DB.User.findOne({ _id: req.params.userId });
if (!user) {
return next(PopulateResponse.notFound());
}
if (user.role === 'admin') {
return next(PopulateResponse.forbidden());
}
// permanently delete
// contact
await DB.Contact.deleteMany({
$or: [{ addedBy: req.params.userId }, { userId: req.params.userId }]
});
// conversation
await DB.Conversation.deleteMany({ memberIds: req.params.userId });
// conversation meta
await DB.ConversationUserMeta.deleteMany({ userId: req.params.userId });
// message
await DB.Message.deleteMany({
$or: [{ senderId: req.params.userId }, { recipientId: req.params.userId }]
});
// device
await DB.Device.deleteMany({ userId: req.params.userId });
// invoice
await DB.Invoice.deleteMany({ userId: req.params.userId });
// transaction
await DB.Transaction.deleteMany({ userId: req.params.userId });
// payout
await DB.PayoutRequest.deleteMany({ modelId: req.params.userId });
// purchase item
const purchaseItems = await DB.PurchaseItem.find({ userId: req.params.userId }).exec();
if (user.type === 'model') {
// sell item - not remove purchase item
const sellItemIds = purchaseItems.map(i => i.sellItemId);
await DB.SellItem.deleteMany({
$and: [{ userId: req.params.userId }, { _id: { $nin: sellItemIds } }]
});
// earning
await DB.Earning.deleteMany({ modelId: req.params.userId });
}
// media - not remove purchase item
const mediaIds = purchaseItems.map(i => i.mediaId);
await DB.Media.deleteMany({
$and: [{ ownerId: req.params.userId }, { _id: { $nin: mediaIds } }]
});
await DB.PurchaseItem.deleteMany({ userId: req.params.userId });
// share love
await DB.ShareLove.deleteMany({
$or: [{ userId: req.params.userId }, { modelId: req.params.userId }]
});
// phone verify
await DB.VerifyCode.deleteMany({ userId: req.params.userId });
// user social
// await DB.UserSocial.deleteMany({ userId: req.params.userId });
await user.remove();
res.locals.remove = { success: true };
return next();
} catch (e) {
return next(e);
}
};
exports.updateProfile = async (req, res, next) => {
try {
const schema = Joi.object().keys({
username: Joi.string().min(3).required(),
gender: Joi.string().allow(['male', 'female', 'transgender']).required(),
bio: Joi.string().min(6).required(),
age: Joi.number().min(0).required(),
address: Joi.string().allow(['', null]).optional(),
city: Joi.string().allow(['', null]).optional(),
state: Joi.string().allow(['', null]).optional(),
country: Joi.string().allow(['', null]).optional(),
phoneNumber: Joi.string().allow(['', null]).optional(),
email: Joi.string().email().required()
});
const validate = Joi.validate(req.body, schema);
if (validate.error) {
return next(PopulateResponse.validationError(validate.error));
}
const user = await DB.User.findOne({ _id: req.user._id }); //? User update profile
if (!user) {
return next(PopulateResponse.error({ msg: 'User is not found!' }));
}
const username = validate.value.username.toLowerCase().trim();
const email = validate.value.email.trim();
const count = await DB.User.count({ $or: [{ username }, { email }], _id: { $ne: user._id } });
if (count) {
return next(PopulateResponse.error({ msg: 'This username or email has been taken!' }));
}
_.merge(user, validate.value);
await user.save();
const isCompletedProfile = await Service.User.updateCompletedProfile(user);
user.isCompletedProfile = isCompletedProfile.success;
res.locals.update = user.getPublicProfile();
next();
} catch (e) {
return next(e);
}
};
exports.updateDocument = async (req, res, next) => {
try {
const schema = Joi.object().keys({
address: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().required(),
country: Joi.string().required(),
firstName: Joi.string().required(),
lastName: Joi.string().required(),
birthday: Joi.string().required(),
instagram: Joi.string().allow([null, '']).optional(),
twitter: Joi.string().allow([null, '']).optional(),
number: Joi.string().required(),
type: Joi.string().allow(['passport', 'ID', 'driverCard']).required(),
zipCode: Joi.string().required(),
isConfirm: Joi.boolean().required(),
isExpired: Joi.boolean().allow([null, '']).default(false).optional(),
expiredDate: Joi.string().allow([null, '']).optional(),
isApproved: Joi.boolean().optional() //? admin approve the document
});
const validate = Joi.validate(req.body, schema);
if (validate.error) {
return next(PopulateResponse.validationError(validate.error));
}
const query = {
_id: req.user.role === 'admin' ? req.params.id : req.user._id
};
const user = await DB.User.findOne(query);
if (!user) {
return next(PopulateResponse.notFound());
}
user.verificationDocument = Object.assign(user.verificationDocument, _.omit(validate.value, ['isApproved']));
user.isCompletedDocument = true;
if (req.user.role === 'admin') {
user.isApproved = validate.value.isApproved || false;
}
await user.save();
res.locals.document = user.verificationDocument;
next();
} catch (e) {
return next(e);
}
};
exports.updateTokenPerMessage = async (req, res, next) => {
try {
const schema = Joi.object().keys({
token: Joi.number().min(1).required()
});
const validate = Joi.validate(req.body, schema);
if (validate.error) {
return next(PopulateResponse.validationError(validate.error));
}
if (req.user.type !== 'model') {
return next(PopulateResponse.forbidden({ message: 'Only models can update!' }));
}
req.user.tokenPerMessage = validate.value.token;
await req.user.save();
res.locals.tokenPerMessage = req.user;
return next();
} catch (e) {
return next(e);
}
};
exports.getOTP = async (req, res, next) => {
try {
const code = process.env.PHONE_DEBUG ? '0000' : Helper.String.randomString(4, '1234567890');
let data = await DB.VerifyCode.findOne({ email: req.user.email });
if (!data) {
data = new DB.VerifyCode({ userId: req.user._id, email: req.user.email });
}
data.code = code;
await data.save();
const siteName = await DB.Config.findOne({ key: SYSTEM_CONST.SITE_NAME });
// send mail with verify code to user
await Service.Mailer.send('verify-code-email.html', req.user.email, {
subject: 'Your verify code',
verifyCode: code.toString(),
siteName: siteName?.value || 'XChat'
});
res.locals.getOTP = PopulateResponse.success({ message: 'Send OTP is successfully!' }, 'OTP_SENT');
return next();
} catch (e) {
return next(e);
}
};
/**
* update model certification photo
*/
exports.updateCertificationPhoto = async (req, res, next) => {
try {
const user = req.params.id
? await DB.User.findOne({
_id: req.params.id,
type: 'model' // only model to update certification
})
: req.user;
if (!user) {
return next(PopulateResponse.notFound());
}
const certificationSize = await DB.Config.findOne({ key: SYSTEM_CONST.CERTIFICATION_SIZE });
if (
!certificationSize ||
!certificationSize.value ||
!certificationSize.value.width ||
!certificationSize.value.height
) {
return PopulateResponse.serverError({ msg: 'Missing certification size!' });
}
// create thumb for the certification
const thumbPath = await Image.resize({
input: req.file.path,
width: certificationSize.value.width || 250,
height: certificationSize.value.height || 250,
resizeOption: '^'
});
const updateString = `verificationDocument.${req.query.position}`;
const update = {
[updateString]: thumbPath
};
await DB.User.update(
{
_id: req.params.id || req.user._id
},
{
$set: update
}
);
// unlink old certification
if (
user.verificationDocument &&
user.verificationDocument[req.query.position] &&
!Helper.String.isUrl(user.verificationDocument[req.query.position]) &&
fs.existsSync(path.resolve(user.verificationDocument[req.query.position]))
) {
fs.unlinkSync(path.resolve(user.verificationDocument[req.query.position]));
}
// remove tmp file
// if (fs.existsSync(path.resolve(req.file.path))) {
// fs.unlinkSync(path.resolve(req.file.path));
// }
res.locals.updateCertificationPhoto = {
url: DB.User.getAvatarUrl(update[updateString]) // convert to string
};
return next();
} catch (e) {
return next(e);
}
};
/**
* User update password
*/
exports.updatePassword = async (req, res, next) => {
const schema = Joi.object().keys({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
newPassword: Joi.string().min(6).required()
});
const validate = Joi.validate(req.body, schema);
if (validate.error) {
return next(PopulateResponse.validationError(validate.error));
}
try {
passport.authenticate('local', async (err, user, info) => {
const error = err || info;
if (error) {
return next(error);
}
if (!user) {
return next(PopulateResponse.notFound());
}
user.password = validate.value.newPassword;
await user.save();
res.locals.updatePassword = {
success: true
};
return next();
})(req, res, next);
} catch (e) {
return next(e);
}
};
/**
* User deactive account yourself
*/
exports.deactiveAccount = async (req, res, next) => {
try {
const user = req.user;
user.isBlocked = true;
await user.save();
res.locals.deactive = PopulateResponse.success(
{ message: 'Your account has been deactived, you will be logged out.' },
'USER_DEACTIVED'
);
next();
} catch (e) {
next(e);
}
};
Model Details Code Samples
import * as React from 'react';
import Head from 'next/head';
import { connect } from 'react-redux';
import { withAuth } from 'lib/withAuth';
import withReduxSaga from 'lib/withReduxSaga';
import { toast } from 'react-toastify';
import Router from 'next/router';
// Actions
import { findContact, resetFindContactStore } from 'lib/contact/actions';
import { loadUser } from 'lib/user/actions';
// Child components
import ContactHeader from 'components/contact/contact-detail-box/header';
import ContactContent from 'components/contact/contact-detail-box/content';
import ContactSearchForm from 'components/contact/contact-search-form';
import UserListing from 'components/user/user-listing';
import UserFilter from 'components/user/filter/user-filter';
import LocationSubFilter from 'components/user/filter/location-sub-filter';
interface IProps {
authUser: any;
addContactStore: {
requesting: boolean;
success: boolean;
error: any;
};
resetFindContactStore: Function;
findContact: Function;
findContactStore: {
requesting: boolean;
success: boolean;
error: any;
contact: any;
isFriend: boolean;
};
removeContact: Function;
removeContactStore: {
requesting: boolean;
success: boolean;
error: any;
};
shareLoveStore: {
requesting: boolean;
success: boolean;
error: any;
};
createConvStore: {
requesting: boolean;
success: boolean;
error: any;
data: any;
};
loadUser: Function;
loadUserStore: {
requesting: boolean;
success: boolean;
error: any;
};
}
interface IState {
isShowLocation: boolean;
gender: string;
country: string;
state: string;
city: string;
}
class ModelListing extends React.Component<IProps, IState> {
constructor(props: any) {
super(props);
this.state = {
isShowLocation: false,
gender: '',
country: '',
state: '',
city: ''
};
}
componentDidMount() {
this.props.resetFindContactStore();
}
componentDidUpdate(prevProps: IProps) {
const { addContactStore, findContactStore, removeContactStore, shareLoveStore, createConvStore, loadUserStore } =
this.props;
//? handle find contact store (only hanlde error)
if (
prevProps.findContactStore.requesting &&
!findContactStore.requesting &&
!findContactStore.success &&
findContactStore.error
) {
return toast.error(findContactStore.error?.data?.message || 'User is not found');
}
//? --- end ---
//? handle add contact store
if (
prevProps.addContactStore.requesting &&
!addContactStore.requesting &&
addContactStore.success &&
!addContactStore.error
) {
return toast.success('Added to your favorites');
}
if (
prevProps.addContactStore.requesting &&
!addContactStore.requesting &&
!addContactStore.success &&
addContactStore.error
) {
return toast.error(addContactStore.error?.data?.message || 'Add to favorites failed!');
}
//? --- end ---
//? handle remove contact store
if (
prevProps.removeContactStore.requesting &&
!removeContactStore.requesting &&
removeContactStore.success &&
!removeContactStore.error
) {
return toast.success('Removed from your favorites');
}
if (
prevProps.removeContactStore.requesting &&
!removeContactStore.requesting &&
!removeContactStore.success &&
removeContactStore.error
) {
return toast.error(removeContactStore.error?.data?.message || 'Remove from favorites failed!');
}
//? --- end ---
//? handle share love store
if (
prevProps.shareLoveStore.requesting &&
!shareLoveStore.requesting &&
shareLoveStore.success &&
!shareLoveStore.error
) {
return toast.success('Tip sent successfully!');
}
if (
prevProps.shareLoveStore.requesting &&
!shareLoveStore.requesting &&
!shareLoveStore.success &&
shareLoveStore.error
) {
return toast.error(shareLoveStore.error?.data?.message || 'Show love fail!');
}
//? --- end ---
//? handle create conversation store
if (
prevProps.createConvStore.requesting &&
!createConvStore.requesting &&
createConvStore.success &&
!createConvStore.error &&
createConvStore.data
) {
Router.push('/message/' + createConvStore.data?._id);
return;
}
if (
prevProps.createConvStore.requesting &&
!createConvStore.requesting &&
!createConvStore.success &&
createConvStore.error
) {
return toast.error(createConvStore.error?.data?.message || 'Create conversation fail!');
}
//? --- end ---
//? handle load user store (only handle error)
if (
prevProps.loadUserStore.requesting &&
!loadUserStore.requesting &&
!loadUserStore.success &&
loadUserStore.error
) {
return toast.error(loadUserStore.error?.data?.message || 'Load user fail!');
}
//? --- end ---
}
async loadUser(query: any) {
const { gender, country, state, city } = this.state;
this.props.loadUser({ ...query, type: 'model', gender, country, state, city });
}
checkProfile = (username: string ) => {
// if (!isFriend) {
// return toast.error('You must add this model to favorite to view his/her profile');
// }
Router.push(
{ pathname: '/contact/detail', query: { username } },
'/contact/detail/'
);
}
render() {
const { findContactStore, authUser } = this.props;
const { isShowLocation } = this.state;
return (
<React.Fragment>
<Head>
<title>Models Listing</title>
</Head>
<main className="main scroll">
<div className="chats">
<div className="chat-body p-3">
<div className="row m-0 ">
<div className="col-md-4 col-xs-12">
<h4 className="font-weight-semibold">Models listing</h4>
</div>
<div className="col-md-8 col-xs-12 mb-2">
<div className="search-filter">
{authUser.type === 'user' && (
<UserFilter onFilter={this.loadUser.bind(this)} isShowLocation={this.setState.bind(this)} />
)}
<ContactSearchForm findContact={this.props.findContact.bind(this)} />
</div>
</div>
{authUser.type === 'user' && isShowLocation && (
<div className="col-md-12 align-self-end mb-2">
<div className="search-filter location-filter">
<LocationSubFilter onFilter={this.loadUser.bind(this)} onLocation={this.setState.bind(this)} />
</div>
</div>
)}
{findContactStore.contact && (
<div className="col-md-12 col-12 mt-2">
<div className="container-xl p-0">
<ContactHeader contact={findContactStore.contact} isFriend={findContactStore.isFriend} />
<div className="row friends-info">
<div className="col">
<ContactContent contact={findContactStore.contact} />
</div>
</div>
</div>
</div>
)}
</div>
<UserListing loadMoreUser={this.loadUser.bind(this)} checkProfile={this.checkProfile.bind(this)} />
</div>
</div>
</main>
</React.Fragment>
);
}
}
const mapStatetoProps = (state: any) => {
const { shareLoveStore, addContactStore, findContactStore, removeContactStore } = state.contact;
return {
shareLoveStore,
addContactStore,
findContactStore,
removeContactStore,
createConvStore: state.conversation.createConvStore,
...state.user,
authUser: state.auth.authUser
};
};
const mapDispatch = {
findContact,
loadUser,
resetFindContactStore
};
export default withReduxSaga(withAuth(connect(mapStatetoProps, mapDispatch)(ModelListing))) as any;