@desco/sequelize-permission-resources
Version:
System, implemented in Sequelize, allowing users and groups of users to access resources
359 lines (279 loc) • 9.87 kB
JavaScript
const moment = require('moment')
const objectMap = require('object.map')
const jsonwebtoken = require('jsonwebtoken')
// login/permissão google
module.exports = params => {
const {
express,
op,
User,
Group,
Permission,
secret,
googleId,
googleKey,
googleScope,
googlePrompt,
google = false,
algorithm = 'HS256',
pswProp = 'password',
loginProp = 'mail',
mailProp = 'mail',
tokenProp = 'token',
tokenTypeProp = 'tokenType',
resourceProp = 'resource',
allowProp = 'allow',
userPkProp = 'id',
groupPkProp = 'id',
expireTokenProp = 'expireToken',
urlLogin = '/login',
googleURL = '/oauth/google',
googleCallbackbURL = '/oauth/google/callback',
expireToken = () => moment().add(1, 'hours').unix(),
loginCallback = i => i,
invalidToken = { message: 'Invalid Token', },
noToken = { message: 'No Token', },
invalidPermission = { message: 'Invalid Permissions', },
expiredToken = { message: 'Expired Token', },
} = params
const throughUserGroup = [User.name, Group.name,].sort().join('_')
User.belongsToMany(Group, { through: throughUserGroup, })
Group.belongsToMany(User, { through: throughUserGroup, })
User.hasMany(Permission)
Permission.belongsTo(User)
Group.hasMany(Permission)
Permission.belongsTo(Group)
User.login = async (login, password) => {
if (!password) return null
const operationParams = {
where: {
[loginProp]: login,
[pswProp]: password,
},
}
return User.findOne(operationParams).then(response => {
if (!response) return null
response = response.toJSON()
delete response[tokenProp]
const userData = {
...response,
[tokenTypeProp]: 'default',
[expireTokenProp]: expireToken(),
}
userData[tokenProp] = jsonwebtoken.sign(
{
[this.userPkProp]: userData[this.userPkProp],
'date': new Date(),
},
secret, { algorithm,
})
return User.update(userData, { where: { [userPkProp]: userData[userPkProp], }, })
.then(() => {
return loginCallback(userData)
})
})
}
User.googleLogin = async req => {
const GoogleAPI = newGoogleAPI(req, {
googleCallbackbURL,
googleId,
googleKey,
})
return GoogleAPI.generateAuthUrl({ scope: googleScope, prompt: googlePrompt, })
}
User.googleCallback = async req => {
const GoogleAPI = newGoogleAPI(req, {
googleCallbackbURL,
googleId,
googleKey,
})
try {
const tokens = await GoogleAPI.setCredentials(req.query.code)
const token = {
[tokenProp]: tokens.access_token,
[tokenTypeProp]: 'google',
[expireTokenProp]: tokens.expiry_date,
}
const info = await GoogleAPI.userInfo()
let [userData, created,] = await User.selectOrCreate({
where: {
[mailProp]: info.data.email,
},
create: {
[mailProp]: info.data.email,
...token,
},
})
if (!created) {
userData = await User.change(token, userData[userPkProp])
}
return { ...token, ...userData, }
}
catch (e) {
return { message: 'Error in login on Google', error: e, }
}
}
Permission.isAllowed = async resource => {
const nativeAllowed = [
treatResource(urlLogin),
treatResource(googleURL),
treatResource(googleCallbackbURL),
]
if (nativeAllowed.indexOf(treatResource(resource)) !== -1) return true
const permissions = await Permission.findAndCountAll({
where: {
[resourceProp]: resource,
[User.name + userPkProp]: null,
[Group.name + groupPkProp]: null,
},
})
return permissions.count > 0
}
Permission.checkToken = async (req, resource) => {
const { token, tokentype: tokenType, } = req.headers
if ([urlLogin, urlLogin + '/',].indexOf(resource) !== -1) return { userId: null, }
if (!token) return { tokenError: noToken, }
if (tokenType === 'google') {
const { Google, } = require('@desco/social-auth')
if (!(await Google.checkToken(token))) return { tokenError: invalidToken, }
const userData = (await User.findOne({ where: { [tokenProp]: token, }, })).toJSON()
return { userId: userData[userPkProp], }
}
else {
if (!jsonwebtoken.verify(token, secret, { algorithm, })) return { tokenError: invalidToken, }
const tokenData = jsonwebtoken.decode(token)
if (moment().unix() > tokenData[expireTokenProp]) return { tokenError: expiredToken, }
const userData = await User.findOne({ where: { [userPkProp]: tokenData[userPkProp], }, })
.then(r => r ? r.toJSON() : {})
if (userData[tokenProp] !== token) return { tokenError: invalidToken, }
return { userId: tokenData[userPkProp], }
}
}
Permission.check = async (userId, resource, req) => {
if (userId === null) return true
resource = treatResource(resource).split('|')[1]
const urlPattern = require('url-pattern')
const permissions = await Permission.list(userId)
const route = Object.keys(permissions)
.filter(i => {
i = i.split('|')
const containMethod = i.length === 2
let method = containMethod ? i[0] : 'ALL'
let url = containMethod ? i[1] : i[0]
url = new urlPattern(url)
return url.match(resource) !== null && ['ALL', req.method,].indexOf(method) !== -1
})[0]
return permissions[route]
}
Permission.list = async (userId = null) => {
const permissions = { allow: {}, user: {}, group: {}, }
const user = userId
? (
(await User.findOne({
where: {
[userPkProp]: userId,
},
include: [Group,],
}))
.toJSON()
)
: null
const groupIds = user ? Object.values(user).reverse()[0].map(i => i[groupPkProp]) : []
const permissionsAllowList = (await Permission.findAll({
where: {
[User.name + userPkProp]: null,
[Group.name + userPkProp]: null,
},
}))
const permissionsGroupList = (await Permission.findAll({
where: {
[Group.name + groupPkProp]: { [op.in]: groupIds, },
},
}))
const permissionsUserList = (await Permission.findAll({
where: {
[User.name + userPkProp]: userId,
},
}))
permissionsAllowList.map(i => {
if (permissions.allow[i[resourceProp]] === false) return
if (i[allowProp] === null && permissions.allow[i[resourceProp]] !== null) return
i = i.toJSON()
permissions.allow[i[resourceProp]] = i[allowProp]
})
permissionsGroupList.map(i => {
if (permissions.group[i[resourceProp]] === false) return
if (i[allowProp] === null && permissions.group[i[resourceProp]] !== null) return
i = i.toJSON()
permissions.group[i[resourceProp]] = i[allowProp]
})
permissionsUserList.map(i => {
if (permissions.user[i[resourceProp]] === false) return
if (i[allowProp] === null && permissions.user[i[resourceProp]] !== null) return
i = i.toJSON()
permissions.user[i[resourceProp]] = i[allowProp]
})
const permissionsObj = permissions.allow
objectMap(permissions.group, (v, k) => {
if (v[allowProp] === null && typeof permissionsObj[k] === 'boolean') return
permissionsObj[treatResource(k)] = v
})
objectMap(permissions.user, (v, k) => {
if (v[allowProp] === null && typeof permissionsObj[k] === 'boolean') return
permissionsObj[treatResource(k)] = v
})
return permissionsObj
}
express.use(async (req, res, next) => {
const resource = treatResource(req.url)
if (await Permission.isAllowed(resource)) {
next()
return
}
const { userId, tokenError, } = await Permission.checkToken(req, resource)
const permissionError = !(await Permission.check(userId, resource, req))
if (permissionError) {
if (tokenError) {
res.json(tokenError)
}
else {
res.json(invalidPermission)
}
}
else {
next()
}
})
express.post(urlLogin, async (req, res) => {
res.json(await User.login(req.body[loginProp], req.body[pswProp]))
})
if (google) {
express.get(googleURL, async (req, res) => {
res.json({ url: await User.googleLogin(req), })
})
express.use(googleCallbackbURL, async (req, res, next) => {
res.locals = await User.googleCallback(req)
next()
})
}
}
function newGoogleAPI(req, { googleCallbackbURL, googleId, googleKey, }) {
const { Google, } = require('@desco/social-auth')
const protocol = `${req.protocol}://`
const host = req.headers.host.slice(-1) === '/' ? req.headers.host : req.headers.host + '/'
const url = googleCallbackbURL[0] === '/' ? googleCallbackbURL.slice(1) : googleCallbackbURL
return new Google({
id: googleId,
key: googleKey,
callbackUrl: `${protocol}${host}${url}`,
})
}
function treatResource(resource) {
resource = resource.split('?')[0]
resource = resource.slice(-1) === '/' ? resource.slice(0, -1) : resource
resource = resource[0] === '/' ? resource.slice(1) : resource
resource = resource.split('|')
const method = resource.length === 1 ? 'ALL' : resource[0]
const url = (resource.length === 1 ? resource[0] : resource[1]).toLowerCase()
return `${method}|${url}`
}