Created
March 31, 2021 20:36
-
-
Save duard/3ca1523db86b0ef520395c828caf1de7 to your computer and use it in GitHub Desktop.
filter users-domain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AggregateRoot from '~sdk-ddd/aggregate-root' | |
import profiles from '~enum/profiles' | |
import KeycloakProvider, { | |
KeycloakAdapter | |
} from '~providers/keycloak-provider' | |
import { UploadFile } from '~handlers' | |
import { ResourceNotFound, Forbidden, ResourceDuplicated } from '~errors' | |
import HttpService from '~sdk-http/http-service' | |
// import { ioc } from '@adonisjs/fold' | |
const AWS = require('aws-sdk') | |
/** | |
* @class The UsersDomain is responsible for user problemns | |
*/ | |
export default class UsersDomain extends AggregateRoot { | |
/** | |
* @constructs UsersDomain | |
* @param {Object} repository | |
*/ | |
constructor () { | |
super() | |
this.aggregates(['keycloaks', 'products']) | |
this.repositories(['users', 'keycloaks', 'clients']) | |
} | |
convertSegment (user) { | |
if ( | |
user.attributes['b2b-api-business-unit'] && | |
user.attributes['b2b-api-business-unit'].includes('AUTOMOTIVA') | |
) { | |
user.attributes['b2b-api-business-unit'] = user.attributes['b2b-api-business-unit'].map((value) => { | |
if (value === 'AUTOMOTIVA') value = 'AUTOPECAS' | |
return value | |
}) | |
} | |
} | |
/** | |
* Create a new user on hub and keycloak | |
* @param {Object} data - the user data | |
*/ | |
async createUser (data) { | |
// get product to populare the email function | |
const selectedProduct = data.profile.products.find( | |
(element) => element.sendEmail === true | |
) | |
let product = null | |
if (selectedProduct && selectedProduct.product) { | |
product = await this.repository('products').findOne({ | |
_id: selectedProduct.product | |
}) | |
} | |
if (!product || !product.config.email.templates.length) { | |
product = await this.repository('products').findOne({ | |
name: 'Portal Yandeh' | |
}) | |
} | |
// convert products into clients -> roles | |
data.profile.clients = await this.aggregate('products').getProfiles( | |
data.profile.products | |
) | |
// buscamos o usuário no DW | |
let user = await this.repository('users').findOneAndPopulate({ | |
username: data.username.trim() | |
}) | |
// buscamos o usuário no keycloak | |
const keycloak = await new KeycloakProvider() | |
let [keycloakUser] = await keycloak.users.findOne({ | |
username: data.username.trim() | |
}) | |
// se usuário existe no DW sem a flag deleted=true e existe no keycloak lançamos erro | |
if (user && !user.deleted && keycloakUser) { | |
throw new ResourceDuplicated('user-exists') | |
} | |
// se usuário existe no DW com a flag deleted=true, restauramos o user | |
if (user && user.deleted) user = await this.restoreUser(user._id, data) | |
// se foi informado um ou mais endereços, validamos | |
if (data.addresses) { | |
data.addresses = this.aggregate('addresses').validateAddress( | |
data.addresses | |
) | |
} | |
if ( | |
typeof data.templateVariables === 'object' && | |
Object.keys(data.templateVariables).length | |
) { | |
for (const attribute of Object.keys(data.templateVariables)) { | |
this.addCustomAttribute( | |
data, | |
`template.${attribute}`, | |
data.templateVariables[attribute] | |
) | |
} | |
delete data.templateVariables | |
} | |
// se o usuário não existe no DW inserimos | |
if (!user) { | |
user = await this.repository('users').createUser(data) | |
} | |
// existindo atualizamos | |
delete user.created_at | |
delete user.__v | |
user = await this.repository('users').findByIdAndUpdatePopulate(user._id, { | |
...user, | |
...data | |
}) | |
const userRep = await KeycloakAdapter.from(user).to('keycloak') | |
const kcUser = userRep.representation() | |
this.convertSegment(kcUser) | |
// se usuário existe no keycloak | |
if (keycloakUser) { | |
await keycloak.users.update({ id: keycloakUser.id }, kcUser) | |
} else { | |
keycloakUser = await keycloak.users.create(kcUser) | |
await this.enqueueWelcomeMail(user, product, false) | |
} | |
keycloakUser = await keycloak.users.findOne({ id: keycloakUser.id }) | |
const updateData = { | |
keycloak: { id: keycloakUser.id } | |
} | |
// se veio imagem de perfil fazemos o upload | |
if (data.file) { | |
updateData.image = await UploadFile(data.file, `user/${user._id}`) | |
.Location | |
} | |
const clients = await userRep.roles() | |
const [resp] = await Promise.all([ | |
this.repository('users').findByIdAndUpdatePopulate(user._id, updateData), | |
this.aggregate('keycloaks').addClientRole(keycloakUser.id, clients) | |
]) | |
return resp | |
} | |
/** | |
* Restore a user that have been deleted | |
* @param {String} userId - User identifier | |
* @param {Object} data - User data to update | |
*/ | |
async restoreUser (userId, data) { | |
return this.repository('users') | |
.findOneAndUpdate( | |
{ _id: userId, deleted: { $in: [true, false, null] } }, | |
{ $set: { ...data, deleted: false } }, | |
{ new: true } | |
) | |
.populate('profile.customers') | |
.populate('profile.clients.client') | |
.populate('segment') | |
.lean() | |
} | |
/** | |
* Update a user on hub | |
* @param {Object} data - the user data | |
* @param {Object} options - additional options to the user creation | |
*/ | |
async changeUser (data, { id }) { | |
if (data.profile && data.profile.products) { | |
// @DEPRECATED - transform products to clients with their roles | |
data.profile.clients = await this.aggregate('products').getProfiles( | |
data.profile.products | |
) | |
} | |
// buscamos o usuário no DW | |
let user = await this.repository('users').findOneAndPopulate({ _id: id }) | |
// buscamos o usuário no keycloak | |
const keycloak = await new KeycloakProvider() | |
let [keycloakUser] = await keycloak.users.findOne({ | |
username: user.username.trim() | |
}) | |
// se usuário existe no DW com a flag deleted=true, restauramos o user | |
if (user && user.deleted) user = await this.restoreUser(user._id, data) | |
if ( | |
typeof data.templateVariables === 'object' && | |
Object.keys(data.templateVariables).length | |
) { | |
for (const attribute of Object.keys(data.templateVariables)) { | |
this.addCustomAttribute( | |
data, | |
`template.${attribute}`, | |
data.templateVariables[attribute] | |
) | |
} | |
} | |
// eslint-disable-next-line | |
["__v", "created_at", "updated_at"].forEach((key) => delete user[key]); | |
user = await this.repository('users').findByIdAndUpdatePopulate(user._id, { | |
...user, | |
...data | |
}) | |
const userRep = await KeycloakAdapter.from(user).to('keycloak') | |
const kcUser = userRep.representation() | |
this.convertSegment(kcUser) | |
// se usuário existe no keycloak | |
if (keycloakUser) { | |
await keycloak.users.update({ id: keycloakUser.id }, kcUser) | |
} else { | |
keycloakUser = await keycloak.users.create(kcUser) | |
} | |
keycloakUser = await keycloak.users.findOne({ id: keycloakUser.id }) | |
if (keycloakUser.enabled !== data.active) { | |
await keycloak.users.update( | |
{ id: keycloakUser.id }, | |
{ enabled: data.active } | |
) | |
} | |
const updateData = { | |
keycloak: { id: keycloakUser.id } | |
} | |
// se veio imagem de perfil fazemos o upload | |
if (data.file) { | |
updateData.image = ( | |
await UploadFile(data.file, `user/${user._id}`) | |
).Location | |
} | |
const clients = await userRep.roles() | |
const products = await this.repository('products') | |
.find() | |
.populate('internals.clients.client') | |
const [updatedUser] = await Promise.all([ | |
this.repository('users').findByIdAndUpdatePopulate(user._id, updateData), | |
this.aggregate('keycloaks').updateClientRole( | |
keycloakUser.id, | |
clients, | |
products | |
) | |
]) | |
return updatedUser | |
} | |
/** | |
* Adiciona um novo atributo customizado ao usuário removendo duplicidades | |
* | |
* @param {Object} user - Objeto representacional do usuário | |
* @param {String} key - String da chave do atributo customizavel | |
* @param {Any} value - Valor | |
*/ | |
addCustomAttribute (user, key, value) { | |
if (!user.custom_attrs) user.custom_attrs = [] | |
const index = user.custom_attrs.findIndex((attr) => attr.key === key) | |
if (index > -1) { | |
if (!user.custom_attrs[index].value.includes(value)) { | |
user.custom_attrs[index].value += `,${value}` | |
} | |
return | |
} | |
user.custom_attrs.push({ key, value }) | |
} | |
/** | |
* List all users according to authenticated user profile | |
* @param {Object} user - instanceof user collection | |
* @param {Object} options - limit and page params | |
* @param {Number} options.limit - pagination limit | |
* @param {Number} options.page - pagination page | |
*/ | |
async getUsers (user, options) { | |
let { limit, page, sort } = { | |
...HttpService.defaultPagination, | |
...options | |
} | |
const roles = user.getRolesForClient(process.env.CLIENT_ID) // roles from logged user | |
// customer user can't list users | |
if (roles.includes(profiles.CUSTOMER_USER)) throw new Forbidden() | |
const params = await this.getFilterFromParams(options) | |
if (roles.includes(profiles.CUSTOMER_ADMIN)) { | |
params['profile.customers'] = { | |
$in: user.profile.customers.map((customer) => customer._id) | |
} | |
} | |
if (roles.includes(profiles.BUSINESS)) params.segment = user.segment | |
const usersPaginationPipeline = (filter = {}, skip = 0, limit = 10, sort = {}) => [ | |
{ $lookup: { from: 'segments', localField: 'segment', foreignField: '_id', as: 'segments' } }, | |
{ $lookup: { from: 'products', localField: 'profile.products.product', foreignField: '_id', as: 'products' } }, | |
{ $lookup: { from: 'clients', localField: 'profile.clients.client', foreignField: '_id', as: 'clients' } }, | |
{ $lookup: { from: 'customers', localField: 'profile.customers', foreignField: '_id', as: 'customers' } }, | |
{ | |
$match: { | |
...filter, | |
active: { '$in': [true, null] }, | |
deleted: { '$in': [false, null] } | |
} | |
}, | |
{ $sort: { ...sort, created_at: -1 } }, | |
{ | |
'$project': { | |
_id: 1, | |
name: 1, | |
segments: 1, | |
keycloak: 1, | |
username: 1, | |
email: 1, | |
token: 1, | |
custom_attrs: 1, | |
'profile.products.profile': 1, | |
'profile.products._id': 1, | |
'profile.products.product': { $arrayElemAt: ['$products', 0] }, | |
'profile.clients.roles': 1, | |
'profile.clients._id': 1, | |
'profile.clients.client': { $arrayElemAt: ['$clients', 0] }, | |
'profile.customers': { $arrayElemAt: ['$customers', 0] }, | |
deleted: 1, | |
active: 1, | |
created_at: 1, | |
updated_at: 1, | |
// products: 1, | |
__v: 1 | |
} | |
}, | |
{ | |
$facet: { | |
total: [{ | |
$count: 'created_at' | |
}], | |
result: [{ | |
$addFields: { | |
_id: '$_id' | |
} | |
}] | |
} | |
}, | |
{ $unwind: '$total' }, | |
{ | |
$project: { | |
result: { | |
$slice: ['$result', skip, { | |
$ifNull: [limit, '$total.created_at'] | |
}] | |
}, | |
meta: { | |
total: '$total.created_at', | |
limit: { $literal: limit }, | |
page: { $literal: ((skip / limit) + 1) }, | |
pages: { | |
$ceil: { | |
$divide: ['$total.created_at', limit] | |
} | |
} | |
} | |
} | |
} | |
] | |
const skip = (limit * page - limit) | |
const executePagination = async () => { | |
return this.repository('users').aggregate(usersPaginationPipeline(params, skip, limit, sort)) | |
} | |
const exec = await executePagination() | |
const data = exec.shift() | |
// let users = await this.repository('users').find({ '_id': '603d4c44505a51001c9b936f' }) | |
// .populate('profile.customers') | |
// .populate('profile.clients.client') | |
// .populate('profile.products.product') | |
// .skip((limit * page) - limit) | |
// .limit(parseInt(limit)) | |
// console.log('users', users) | |
return { | |
docs: data.result, | |
total: data.meta.total, | |
page: data.meta.page, | |
limit: data.meta.limit, | |
pages: data.meta.pages | |
} | |
} | |
/** | |
* Create a mongodb filter format from parameters | |
* @param {Object} params - filters to apply in key->value format | |
*/ | |
async getFilterFromParams (params) { | |
let filter = {} | |
const searchParams = [] | |
let searchFilter = {} | |
// old endpoint without filters | |
if (params.q) { | |
let queryString = params.q.replace(/(\+|\*|\.)/, '\\$1') | |
queryString = new RegExp(`${queryString}`, 'i') | |
filter['$or'] = [{ name: queryString }, { email: queryString }] | |
return filter | |
} | |
if (params.search) { | |
let queryString = params.search.replace(/(\+|\*|\.)/, '\\$1') | |
queryString = new RegExp(`${params.search}`, 'i') | |
searchFilter = {} | |
searchFilter[`${params.searchBy}`] = { '$in': [queryString] } | |
searchParams.push(searchFilter) | |
} | |
if (params.segment) { | |
let queryString = params.segment | |
searchFilter = {} | |
searchFilter[`segments.name`] = { '$in': queryString.split(',') } | |
searchParams.push(searchFilter) | |
} | |
if (params.search) { | |
let queryString = params.search.replace(/(\+|\*|\.)/, '\\$1') | |
queryString = new RegExp(`${params.search}`, 'i') | |
searchFilter = {} | |
searchFilter[`${params.searchBy}`] = { '$in': [queryString] } | |
searchParams.push(searchFilter) | |
} | |
let resultParams = {} | |
/* | |
if (params.profile) { | |
const criterias = [] | |
for (const product of Object.keys(params.profile)) { | |
criterias.push({ | |
$and: [ | |
{ name: new RegExp(product, 'i') }, | |
{ | |
'internals.name': { | |
$in: params.profile[product].map(prof => new RegExp(prof, 'i')) | |
} | |
} | |
] | |
}) | |
} | |
const products = await (ioc.use('repositories/products-repository')).find({ $or: criterias }) | |
for (const product of products) { | |
const filter = { $and: [] } | |
filter['$and'].push({ 'profile.products.product': String(product._id) }) | |
filter['$and'].push({ | |
'profile.products.profile': { | |
'$in': product.internals | |
.filter(internal => params.profile[product.name].includes(internal.name)) | |
.reduce((acc, profile) => { | |
acc.push(profile._id) | |
return acc | |
}, []) | |
} | |
}) | |
console.log('\n', filter, '\n') | |
searchParams.push(filter) | |
} | |
if (!params['$or']) params['$or'] = [] | |
searchParams['$or'].push(...searchParams) | |
} | |
*/ | |
for (let index = 0; index < searchParams.length; index++) { | |
const element = searchParams[index] | |
resultParams = Object.assign(resultParams, element) | |
console.log(index, element) | |
} | |
return resultParams | |
} | |
/** | |
* | |
* @param {String} id | |
*/ | |
async getUserById (id) { | |
const user = await this.repository('users').findByIdAndPopulate(id) | |
if (!user) throw new ResourceNotFound('User not found') | |
return user | |
} | |
/** | |
* | |
* @param {User} user | |
*/ | |
async removeUser (user) { | |
await this.repository('users').removeUser(user._id) | |
await this.aggregate('keycloaks').removeUser(user) | |
} | |
/** | |
* | |
* @param {*} data | |
* @param {*} id | |
*/ | |
async changeUserLogged (data, id) { | |
if (data.token && data.topic) { | |
await this.aggregate('notifications').subscribe(data.token, data.topic) | |
} | |
if (data.file) data.image = (await UploadFile(data.file, 'user')).Location | |
const user = await this.repository('users').findByIdAndUpdatePopulate( | |
id, | |
data | |
) | |
return user | |
} | |
/** | |
* Enqueue welcome mail to be sended to user | |
* @param {Object} user - User from hub | |
* @param {Object} product - Product with email template | |
* @param {Boolean} passwordDefinition - Forces password definition mail | |
*/ | |
async enqueueWelcomeMail (user, product, passwordDefinition = false) { | |
AWS.config.update({ region: 'us-east-1' }) | |
return new AWS.DynamoDB() | |
.putItem({ | |
TableName: process.env.MAILER_TABLE, | |
Item: { | |
id: { S: `${user.email}-${new Date().getTime()}` }, | |
email: { S: user.email }, | |
payload: { S: JSON.stringify({ user, products: [product] }) }, | |
createdAt: { S: new Date().toISOString() }, | |
updatedAt: { S: new Date().toISOString() }, | |
passwordDefinition: { BOOL: passwordDefinition || false }, | |
status: { S: 'PENDING' } | |
} | |
}) | |
.promise() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment