/* eslint-disable camelcase */ /* eslint-disable no-param-reassign */ import { Model, DataTypes, Op } from 'sequelize'; import { values, pick, isEqual, isNil, omitBy } from 'lodash'; import { hash, compare } from 'bcryptjs'; import moment from 'moment-timezone'; import httpStatus from 'http-status'; import { v4 as uuidv4 } from 'uuid'; import APIError from '../utils/APIException'; import postgres from '../../config/postgres'; import { serviceName } from '../../config/vars'; import Permissions from '../../common/utils/Permissions'; /** * Create connection */ class User extends Model { } const { sequelize } = postgres; User.Providers = { APPLE: 'apple', GOOGLE: 'google', FACEBOOK: 'facebook', }; User.Statuses = { INACTIVE: 'inactive', ACTIVE: 'active', BANNED: 'banned' }; User.NameStatuses = { INACTIVE: 'inactive', ACTIVE: 'active', BANNED: 'banned' }; User.Genders = { MALE: 'male', FEMALE: 'female' }; User.Services = { USER: 'user', STAFF: 'staff', SERVICE: 'service' }; User.Types = { STAFF: 'staff', COMPANY: 'company', SUPPLIER: 'supplier', INDIVIDUAL: 'individual' }; const PUBLIC_FIELDS = [ 'type', 'role', 'name', 'note', 'phone', 'email', 'avatar', 'cover', 'group', 'stores', 'gender', 'birthday', 'barcode', 'tax_code', 'company', 'country', 'address', 'province', 'district', 'ward', 'password', 'permissions' ]; const PUBLIC_PROFILE_FIELDS = [ 'name', 'note', 'phone', 'email', 'avatar', 'cover', 'gender', 'birthday', 'barcode', 'password', 'tax_code', 'company', 'country', 'address', 'province', 'district', 'ward' ]; /** * User Schema * @public */ User.init( { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, type: { type: DataTypes.STRING(10), defaultValue: User.Types.INDIVIDUAL }, name: { type: DataTypes.STRING(155), allowNull: false }, note: { type: DataTypes.STRING(255), defaultValue: null }, phone: { type: DataTypes.STRING(20), defaultValue: null }, email: { type: DataTypes.STRING(255), validate: { isEmail: true }, defaultValue: null }, avatar: { type: DataTypes.STRING(255), defaultValue: null }, cover: { type: DataTypes.STRING(255), defaultValue: null }, birthday: { type: DataTypes.DATE, defaultValue: null }, barcode: { type: DataTypes.STRING(255), defaultValue: null }, gender: { type: DataTypes.STRING(50), values: values(User.Genders), defaultValue: User.Genders.FEMALE }, tax_code: { type: DataTypes.STRING(25), defaultValue: null }, company: { type: DataTypes.STRING(100), defaultValue: null }, country: { type: DataTypes.STRING(3), defaultValue: 'vn' }, language: { type: DataTypes.STRING(3), default: 'vi' }, timezone: { type: DataTypes.INTEGER, default: 7 }, address: { type: DataTypes.STRING(255), defaultValue: null }, province: { type: DataTypes.JSONB, defaultValue: null // id || name }, district: { type: DataTypes.JSONB, defaultValue: null // id || name }, ward: { type: DataTypes.JSONB, defaultValue: null // id || name }, role: { type: DataTypes.JSONB, defaultValue: null // id | name }, group: { type: DataTypes.JSONB, defaultValue: null // id | name }, status: { type: DataTypes.STRING(50), values: values(User.Statuses), defaultValue: User.Statuses.ACTIVE }, status_name: { type: DataTypes.STRING(50), values: values(User.NameStatuses), defaultValue: User.NameStatuses.ACTIVE }, // stores: { // type: DataTypes.JSONB, // defaultValue: [] // id, name, phone, address // }, service: { type: DataTypes.STRING(50), values: values(User.Services), defaultValue: User.Services.USER }, password: { type: DataTypes.STRING(255), defaultValue: uuidv4() }, permissions: { type: DataTypes.ARRAY(DataTypes.STRING(155)), defaultValue: [Permissions.USER] }, // third party account apple: { type: DataTypes.JSONB, defaultValue: { id: null, email: null, name: null, avatar: null, } }, facebook: { type: DataTypes.JSONB, defaultValue: { id: null, email: null, name: null, avatar: null, } }, google: { type: DataTypes.JSONB, defaultValue: { id: null, email: null, name: null, avatar: null, } }, normalize_name: { type: DataTypes.STRING(255), defaultValue: null }, normalize_dob: { type: DataTypes.INTEGER, defaultValue: null }, normalize_mob: { type: DataTypes.INTEGER, defaultValue: null }, // metric // config is_active: { type: DataTypes.BOOLEAN, defaultValue: true }, device_id: { type: DataTypes.STRING(255), defaultValue: 'unkown' }, ip_address: { type: DataTypes.STRING(12), defaultValue: 'unkown' }, is_verified_phone: { type: DataTypes.BOOLEAN, defaultValue: false }, is_verified_email: { type: DataTypes.BOOLEAN, defaultValue: false }, is_verified_password: { type: DataTypes.BOOLEAN, defaultValue: false }, created_at: { type: DataTypes.DATE, defaultValue: () => new Date() }, updated_at: { type: DataTypes.DATE, defaultValue: () => new Date() }, created_by: { type: DataTypes.JSONB, allowNull: false, defaultValue: { id: null, name: null } } }, { timestamps: false, sequelize: sequelize, schema: serviceName, tableName: 'tbl_users', modelName: 'user' } ); /** * Register event emiter */ User.Events = { USER_CREATED: `${serviceName}.user.created`, USER_UPDATED: `${serviceName}.user.updated`, USER_DELETED: `${serviceName}.user.deleted`, }; User.EVENT_SOURCE = `${serviceName}.user`; /** * Add your * - pre-save hooks * - validations * - virtuals */ User.addHook('beforeCreate', async (model) => { const user = model; if (user.password) { const rounds = 10; user.password = await hash(user.password, rounds); console.log("pass created"); } return user; }); User.addHook('beforeUpdate', async (model, options) => { const user = model; const { password } = options; // has pass if (password) { const rounds = 10; user.password = await hash(password, rounds); } return user; }); User.addHook('afterCreate', () => { }); User.addHook('afterUpdate', () => { }); User.addHook('afterDestroy', () => { }); /** * Check min or max in condition * @param {*} options * @param {*} field * @param {*} type */ function checkMinMaxOfConditionFields(options, field, type = 'Number') { let _min = null; let _max = null; // Transform min max if (type === 'Date') { _min = new Date(options[`min_${field}`]); _min.setHours(0, 0, 0, 0); _max = new Date(options[`max_${field}`]); _max.setHours(23, 59, 59, 999); } if (type === 'Number') { _min = parseFloat(options[`min_${field}`]); _max = parseFloat(options[`max_${field}`]); } // Transform condition if ( !isNil(options[`min_${field}`]) || !isNil(options[`max_${field}`]) ) { if ( options[`min_${field}`] && !options[`max_${field}`] ) { options[field] = { [Op.gte]: _min }; } else if ( !options[`min_${field}`] && options[`max_${field}`] ) { options[field] = { [Op.lte]: _max }; } else { options[field] = { [Op.gte]: _min || 0, [Op.lte]: _max || 0 }; } } // Remove first condition delete options[`max_${field}`]; delete options[`min_${field}`]; } /** * Load query * @param {*} params */ function filterConditions(params) { const options = omitBy(params, isNil); options.is_active = true; if (options.types) { options.type = { [Op.in]: options.types.split(',') }; } delete options.types; if (options.roles) { options['role.id'] = { [Op.in]: options.roles.split(',') }; } delete options.roles; if (options.groups) { options['group.id'] = { [Op.in]: options.groups.split(',') }; } delete options.groups; if (options.statuses) { options.status = { [Op.in]: options.statuses.split(',') }; } delete options.statuses; if (options.services) { options.service = { [Op.in]: options.services.split(',') }; } delete options.services; // console.log("delete services from asfsd"); if (options.genders) { options.gender = { [Op.in]: options.genders.split(',') }; } delete options.genders; if (options.staffs) { options['created_by.id'] = { [Op.in]: options.staffs.split(',') }; } delete options.staffs; if (options.stores) { options.store_path = { [Op.overlap]: options.stores.split(',') }; } delete options.stores; if (options.provinces) { options['province.id'] = { [Op.in]: options.provinces.split(',') }; } delete options.provinces; if (options.keyword) { options.normalize_name = { [Op.iLike]: `%${options.keyword}%` }; } delete options.keyword; // Date Filter checkMinMaxOfConditionFields(options, 'created_at', 'Date'); return options; } /** * Load sort query * @param {*} sort_by * @param {*} order_by */ function sortConditions({ sort_by, order_by }) { let sort = null; switch (sort_by) { case 'created_at': sort = ['created_at', order_by]; break; case 'updated_at': sort = ['updated_at', order_by]; break; case 'total_debt': sort = ['total_debt', order_by]; break; case 'total_point': sort = ['total_point', order_by]; break; case 'total_purchase': sort = ['total_purchase', order_by]; break; case 'total_invoice_price': sort = ['total_invoice_price', order_by]; break; case 'total_return_price': sort = ['total_return_price', order_by]; break; default: sort = ['created_at', 'DESC']; break; } return sort; } /** * Transform postgres model to expose object */ User.transform = (params, includeRestrictedFields = true) => { const transformed = {}; const fields = [ 'id', 'name', 'note', 'phone', 'email', 'avatar', 'role', 'cover', 'gender', 'birthday', 'barcode', 'tax_code', 'company', 'country', 'address', 'province', 'district', 'ward', // manager 'is_active', 'is_verified_phone', 'is_verified_email', 'is_verified_password' ]; if (includeRestrictedFields) { const privateFiles = [ 'type', 'role', 'group', 'stores', 'status', 'status_name', 'permissions', 'created_by' ]; fields.push(...privateFiles); }; // console.log(fields + "@@@"); fields.forEach((field) => { transformed[field] = params[field]; }); // pipe decimal const decimalFields = [ 'total_debt', 'total_order_price', 'total_invoice_price', 'total_return_price', 'total_purchase', 'total_point', ]; decimalFields.forEach((field) => { transformed[field] = parseInt(params[field], 0); }); // pipe date const dateFields = [ 'birthday', 'created_at', 'updated_at', 'last_purchase' ]; dateFields.forEach((field) => { transformed[field] = moment(params[field]).unix(); }); // console.log(transformed); return transformed; }; /** * Get all changed properties */ User.getChangedProperties = ({ newModel, oldModel }, includeRestrictedFields = true) => { const changedProperties = []; const allChangableProperties = [ 'name', 'note', 'phone', 'email', 'avatar', 'cover', 'gender', 'birthday', 'barcode', 'tax_code', 'company', 'country', 'address', 'province', 'district', 'ward', ]; if (includeRestrictedFields) { const privateFiles = [ 'type', 'role', 'group', 'password', 'stores', 'status', 'permissions' ]; allChangableProperties.push(...privateFiles); // console.log(allChangableProperties); } if (!oldModel) { // console.log("old model"); return allChangableProperties; } allChangableProperties.forEach((field) => { if (!isEqual(newModel[field], oldModel[field])) { changedProperties.push(field); } }); return changedProperties; }; /** * Check user by phone - email * * @public * @param {object} data email || phone */ User.getUserByPhoneOrEmail = async ({ phone, email, validate = false }) => { try { let user = null; if (phone) { user = await User.findOne({ where: { is_active: true, phone: phone } }); } if (email) { user = await User.findOne({ where: { is_active: true, email: email, } }); } if (!user) { throw new APIError({ status: httpStatus.NOT_FOUND, message: 'Không tìm thấy tài khoản này!' }); } if (validate) { if (user.status === User.Statuses.INACTIVE) { throw new APIError({ message: 'Tài khoản chưa được kích hoạt!', status: httpStatus.UNAUTHORIZED }); } if (user.status === User.Statuses.BANNED) { throw new APIError({ message: 'Tài khoản đã bị khoá!', status: httpStatus.UNAUTHORIZED }); } } return user; } catch (ex) { throw (ex); } }; User.getUserByPhoneOrEmailRegister = async ({ phone, email }) => { try { let user = null; if (phone) { user = await User.findOne({ where: { is_active: true, phone: phone } }); } if (email) { user = await User.findOne({ where: { is_active: true, email: email, } }); } if (user) { throw new APIError({ status: httpStatus.NOT_FOUND, message: 'Tài khoản này đã được đăng kí' }); } // if (validate) { // if (user.status === User.Statuses.INACTIVE) { // throw new APIError({ // message: 'Tài khoản chưa được kích hoạt!', // status: httpStatus.UNAUTHORIZED // }); // } // if (user.status === User.Statuses.BANNED) { // throw new APIError({ // message: 'Tài khoản đã bị khoá!', // status: httpStatus.UNAUTHORIZED // }); // } // } return true; } catch (ex) { throw (ex); } }; User.getStaffPermissions = async (staffId) => { const user = await User.findOne({ attributes: ['permissions'], where: { status: User.Statuses.ACTIVE, is_active: true, id: staffId } }); return user.permissions; }; /** * Get Store By Id * * @public * @param {String} userId */ User.get = async (userId) => { try { const user = await User.findOne({ where: { id: userId, is_active: true } }); // console.log(user); if (isNil(user)) { throw new APIError({ status: httpStatus.NOT_FOUND, message: 'Không tìm thấy người dùng này!' }); } return user; } catch (ex) { throw (ex); } }; /** * List * * @param {number} skip - Number of records to be skipped. * @param {number} limit - Limit number of records to be returned. * @returns {Promise} */ User.list = async ({ roles, types, groups, stores, provinces, staffs, genders, statuses, services, keyword, min_created_at, max_created_at, min_last_purchase, max_last_purchase, min_total_order_price, max_total_order_price, min_total_invoice_price, max_total_invoice_price, min_total_point, max_total_point, min_total_debt, max_total_debt, // sort condition skip = 0, limit = 20, sort_by = 'desc', order_by = 'created_at', }) => { const options = filterConditions({ roles, types, groups, stores, provinces, staffs, genders, statuses, services, keyword, min_created_at, max_created_at, min_last_purchase, max_last_purchase, min_total_order_price, max_total_order_price, min_total_invoice_price, max_total_invoice_price, min_total_point, max_total_point, min_total_debt, max_total_debt, }); const sorts = sortConditions({ sort_by, order_by }); return User.findAll({ where: options, order: [sorts], offset: skip, limit: limit }); }; /** * Total records. * * @param {number} skip - Number of records to be skipped. * @param {number} limit - Limit number of records to be returned. * @returns {Promise} */ User.totalRecords = ({ roles, types, groups, stores, provinces, staffs, genders, statuses, services, keyword, min_created_at, max_created_at, min_last_purchase, max_last_purchase, min_total_order_price, max_total_order_price, min_total_invoice_price, max_total_invoice_price, min_total_point, max_total_point, min_total_debt, max_total_debt, }) => { const options = filterConditions({ roles, types, groups, stores, provinces, staffs, genders, statuses, services, keyword, min_created_at, max_created_at, min_last_purchase, max_last_purchase, min_total_order_price, max_total_order_price, min_total_invoice_price, max_total_invoice_price, min_total_point, max_total_point, min_total_debt, max_total_debt, }); return User.count({ where: options }); }; /** * Check Duplicate Account Info * * @public * @param {object} data email || phone */ User.checkDuplicate = async ({ userId, service, email, phone }) => { let user = null; let message = null; if (phone) { user = await User.findOne({ where: { phone, service, is_active: true, id: userId ? { [Op.ne]: userId } : { [Op.ne]: null } } }); message = '"Phone already in use'; } if (email) { user = await User.findOne({ where: { email, service, is_active: true, id: userId ? { [Op.ne]: userId } : { [Op.ne]: null } } }); message = '"Email" already exist'; } if (user) { throw new APIError({ message: message, errors: [ { field: 'username', location: 'body', messages: [message] } ], status: httpStatus.CONFLICT }); } return null; }; /** * Compare password * * @param {String} password * @param {User} model */ User.passwordMatches = async (model, password) => compare(password, model.password); /** * Check if current user is expired * * @param {Date} currentTime * @param {User} model * @returns {Boolean} */ User.isExpired = (model, currentTime = null) => { const currentTimeToCheck = currentTime !== null ? currentTime : new Date(); return currentTimeToCheck >= model.expired_at; }; /** * Filter only allowed fields from user * * @param {Object} params */ User.filterParams = (params, includeRestrictedFields = true) => { if (includeRestrictedFields) { return pick(params, PUBLIC_FIELDS); } return pick(params, PUBLIC_PROFILE_FIELDS); }; /** * Return fully qualified phone number * * @param {String} phone */ User.normalizePhoneNumber = (phone) => `+84${phone .replace(/\D/g, '') .replace(/^84/, '') .replace(/^0*/, '')}`; /** * @typedef User */ export default User;