mirror of
https://github.com/Hopiu/webapi-eca.git
synced 2026-03-16 22:10:31 +00:00
469 lines
14 KiB
CoffeeScript
469 lines
14 KiB
CoffeeScript
###
|
|
|
|
DB Interface
|
|
============
|
|
> Handles the connection to the database and provides functionalities for
|
|
> event/action modules, rules and the encrypted storing of authentication tokens.
|
|
> General functionality as a wrapper for the module holds initialization,
|
|
> encryption/decryption, the retrieval of modules and shut down.
|
|
>
|
|
> The general structure for linked data is that the key is stored in a set.
|
|
> By fetching all set entries we can then fetch all elements, which is
|
|
> automated in this function.
|
|
> For example modules of the same group, e.g. action modules are registered in an
|
|
> unordered set in the database, from where they can be retrieved again. For example
|
|
> a new action module has its ID (e.g 'probinder') first registered in the set
|
|
> 'action_modules' and then stored in the db with the key 'action\_module\_' + ID
|
|
> (e.g. action\_module\_probinder).
|
|
>
|
|
|
|
###
|
|
|
|
# **Requires:**
|
|
|
|
# - [Logging](logging.html)
|
|
log = require './logging'
|
|
|
|
# - External Modules: [crypto-js](https://github.com/evanvosberg/crypto-js) and
|
|
# [redis](https://github.com/mranney/node_redis)
|
|
crypto = require 'crypto-js'
|
|
redis = require 'redis'
|
|
|
|
###
|
|
Module call
|
|
-----------
|
|
Initializes the DB connection. Requires a valid configuration file which contains
|
|
a db port and a crypto key.
|
|
|
|
@param {Object} args
|
|
###
|
|
exports = module.exports = ( args ) =>
|
|
args = args ? {}
|
|
log args
|
|
config = require './config'
|
|
config args
|
|
@db?.quit()
|
|
if config.isReady()
|
|
@crypto_key = config.getCryptoKey()
|
|
@db = redis.createClient config.getDBPort(),
|
|
'localhost', { connect_timeout: 2000 }
|
|
@db.on 'error', ( err ) ->
|
|
err.addInfo = 'message from DB'
|
|
log.error 'DB', err
|
|
else
|
|
log.error 'DB', 'Initialization failed because of missing config file!'
|
|
|
|
###
|
|
Checks whether the db is connected and passes either an error on failure after
|
|
ten attempts within five seconds, or nothing on success to the callback(err).
|
|
|
|
@public isConnected( *cb* )
|
|
@param {function} cb
|
|
###
|
|
exports.isConnected = ( cb ) =>
|
|
if @db.connected then cb()
|
|
else
|
|
numAttempts = 0
|
|
fCheckConnection = =>
|
|
if @db.connected
|
|
log.print 'DB', 'Successfully connected to DB!'
|
|
cb()
|
|
else if numAttempts++ < 10
|
|
setTimeout fCheckConnection, 100
|
|
else
|
|
cb new Error 'Connection to DB failed!'
|
|
setTimeout fCheckConnection, 100
|
|
|
|
###
|
|
Abstracts logging for simple action replies from the DB.
|
|
|
|
@private replyHandler( *action* )
|
|
@param {String} action
|
|
###
|
|
replyHandler = ( action ) ->
|
|
( err, reply ) ->
|
|
if err
|
|
err.addInfo = "during \"#{ action }\""
|
|
log.error 'DB', err
|
|
else
|
|
log.print 'DB', "#{ action }: #{ reply }"
|
|
|
|
###
|
|
Push an event into the event queue.
|
|
|
|
@public pushEvent( *oEvent* )
|
|
@param {Object} oEvent
|
|
###
|
|
exports.pushEvent = ( oEvent ) =>
|
|
if oEvent
|
|
log.print 'DB', "Event pushed into the queue: #{ oEvent.eventid }"
|
|
@db.rpush 'event_queue', JSON.stringify( oEvent )
|
|
else
|
|
log.error 'DB', 'Why would you give me an empty event...'
|
|
|
|
|
|
###
|
|
Pop an event from the event queue and pass it to the callback(err, obj) function.
|
|
|
|
@public popEvent( *cb* )
|
|
@param {function} cb
|
|
###
|
|
exports.popEvent = ( cb ) =>
|
|
makeObj = ( pcb ) ->
|
|
( err, obj ) ->
|
|
pcb err, JSON.parse( obj )
|
|
@db.lpop 'event_queue', makeObj( cb )
|
|
|
|
###
|
|
Purge the event queue.
|
|
|
|
@public purgeEventQueue()
|
|
###
|
|
exports.purgeEventQueue = () =>
|
|
@db.del 'event_queue', replyHandler 'purging event queue'
|
|
|
|
###
|
|
Hashes a string based on SHA-3-512.
|
|
|
|
@private hash( *plainText* )
|
|
@param {String} plainText
|
|
###
|
|
hash = ( plainText ) =>
|
|
if !plainText? then return null
|
|
try
|
|
( crypto.SHA3 plainText, { outputLength: 512 } ).toString()
|
|
catch err
|
|
err.addInfo = 'during hashing'
|
|
log.error 'DB', err
|
|
null
|
|
|
|
|
|
###
|
|
Encrypts a string using the crypto key from the config file, based on aes-256-cbc.
|
|
|
|
@private encrypt( *plainText* )
|
|
@param {String} plainText
|
|
###
|
|
encrypt = ( plainText ) =>
|
|
if !plainText? then return null
|
|
try
|
|
enciph = crypto.createCipher 'aes-256-cbc', @crypto_key
|
|
et = enciph.update plainText, 'utf8', 'base64'
|
|
et + enciph.final 'base64'
|
|
catch err
|
|
err.addInfo = 'during encryption'
|
|
log.error 'DB', err
|
|
null
|
|
|
|
###
|
|
Decrypts an encrypted string and hands it back on success or null.
|
|
|
|
@private decrypt( *crypticText* )
|
|
@param {String} crypticText
|
|
###
|
|
decrypt = ( crypticText ) =>
|
|
if !crypticText? then return null;
|
|
try
|
|
deciph = crypto.createDecipher 'aes-256-cbc', @crypto_key
|
|
dt = deciph.update crypticText, 'base64', 'utf8'
|
|
dt + deciph.final 'utf8'
|
|
catch err
|
|
err.addInfo = 'during decryption'
|
|
log.error 'DB', err
|
|
null
|
|
|
|
###
|
|
Fetches all linked data set keys from a linking set, fetches the single data objects
|
|
via the provided function and returns the results to the callback(err, obj) function.
|
|
|
|
@private getSetRecords( *set, fSingle, cb* )
|
|
@param {String} set the set name how it is stored in the DB
|
|
@param {function} fSingle a function to retrieve a single data element per set entry
|
|
@param {function} cb the callback(err, obj) function that receives all the retrieved data or an error
|
|
###
|
|
getSetRecords = ( set, fSingle, cb ) =>
|
|
log.print 'DB', "Fetching set records: #{ set }"
|
|
@db.smembers set, ( err, arrReply ) ->
|
|
if err
|
|
err.addInfo = "fetching #{ set }"
|
|
log.error 'DB', err
|
|
else if arrReply.length == 0
|
|
cb()
|
|
else
|
|
semaphore = arrReply.length
|
|
objReplies = {}
|
|
setTimeout ->
|
|
if semaphore > 0
|
|
cb new Error "Timeout fetching #{ set }"
|
|
, 2000
|
|
fCallback = ( prop ) ->
|
|
( err, data ) ->
|
|
--semaphore
|
|
if err
|
|
err.addInfo = "fetching single element: #{ prop }"
|
|
log.error 'DB', err
|
|
else if not data
|
|
log.error 'DB', new Error "Empty key in DB: #{ prop }"
|
|
else
|
|
objReplies[ prop ] = data
|
|
if semaphore == 0
|
|
cb null, objReplies
|
|
fSingle reply, fCallback( reply ) for reply in arrReply
|
|
|
|
###
|
|
## Action Modules
|
|
#TODO Rename Action Modules into something like Action Caller
|
|
###
|
|
|
|
###
|
|
Store a string representation of an action module in the DB.
|
|
|
|
@public storeActionModule ( *amId, data* )
|
|
@param {String} amId
|
|
@param {String} data
|
|
###
|
|
exports.storeActionModule = ( amId, data ) =>
|
|
log.print 'DB', "storeActionModule: #{ amId }"
|
|
@db.sadd 'action-modules', amId, replyHandler "storing action module key #{ amId }"
|
|
@db.set "action-module:#{ amId }", data, replyHandler "storing action module #{ amId }"
|
|
|
|
###
|
|
Query the DB for an action module and pass it to the callback(err, obj) function.
|
|
|
|
@public getActionModule( *amId, cb* )
|
|
@param {String} amId
|
|
@param {function} cb
|
|
###
|
|
exports.getActionModule = ( amId, cb ) =>
|
|
log.print 'DB', "getActionModule: #{ amId }"
|
|
@db.get "action-module:#{ amId }", cb
|
|
|
|
###
|
|
Fetch all action modules and hand them to the callback(err, obj) function.
|
|
|
|
@public getActionModules( *cb* )
|
|
@param {function} cb
|
|
###
|
|
exports.getActionModules = ( cb ) ->
|
|
getSetRecords 'action-modules', exports.getActionModule, cb
|
|
|
|
###
|
|
Store user-specific action module parameters .
|
|
|
|
@public storeActionParams( *userId, amId, data* )
|
|
@param {String} userId
|
|
@param {String} amId
|
|
@param {String} data
|
|
###
|
|
exports.storeActionParams = ( userId, amId, data ) =>
|
|
log.print 'DB', "storeActionParams: #{ amId }:#{ userId }"
|
|
@db.set "action-params:#{ amId }:#{ userId }", hash(data),
|
|
replyHandler "storing action params #{ amId }:#{ userId }"
|
|
|
|
###
|
|
Query the DB for user-specific action module parameters,
|
|
and pass it to the callback(err, obj) function.
|
|
|
|
@public getActionParams( *userId, amId, cb* )
|
|
@param {String} userId
|
|
@param {String} amId
|
|
@param {function} cb
|
|
###
|
|
exports.getActionParams = ( userId, amId, cb ) =>
|
|
log.print 'DB', "getActionParams: #{ amId }:#{ userId }"
|
|
@db.get "action-params:#{ amId }:#{ userId }", ( err, data ) ->
|
|
cb err, decrypt data
|
|
|
|
|
|
###
|
|
## Event Modules
|
|
#TODO rename event modules to event puller or something like that
|
|
###
|
|
|
|
###
|
|
Store a string representation of an event module in the DB.
|
|
|
|
@public storeEventModule( *emId, data* )
|
|
@param {String} emId
|
|
@param {String} data
|
|
###
|
|
exports.storeEventModule = ( emId, data ) =>
|
|
log.print 'DB', "storeEventModule: #{ emId }"
|
|
@db.sadd 'event-modules', emId, replyHandler "storing event module key #{ emId }"
|
|
@db.set 'event-module:#{ emId }', data, replyHandler "storing event module #{ emId }"
|
|
|
|
###
|
|
Query the DB for an event module and pass it to the callback(err, obj) function.
|
|
|
|
@public getEventModule( *emId, cb* )
|
|
@param {String} emId
|
|
@param {function} cb
|
|
###
|
|
exports.getEventModule = ( emId, cb ) =>
|
|
log.print 'DB', "getEventModule: #{ emId }"
|
|
@db.get "event-module:#{ emId }", cb
|
|
|
|
###
|
|
Fetch all event modules and pass them to the callback(err, obj) function.
|
|
|
|
@public getEventModules( *cb* )
|
|
@param {function} cb
|
|
###
|
|
exports.getEventModules = ( cb ) ->
|
|
getSetRecords 'event-modules', exports.getEventModule, cb
|
|
|
|
###
|
|
Store a string representation of user-specific parameters for an event module.
|
|
|
|
@public storeEventParams( *userId, emId, data* )
|
|
@param {String} userId
|
|
@param {String} emId
|
|
@param {Object} data
|
|
###
|
|
# TODO is used, remove unused ones
|
|
exports.storeEventParams = ( userId, emId, data ) =>
|
|
log.print 'DB', "storeEventParams: #{ emId }:#{ userId }"
|
|
# TODO encryption based on user specific key?
|
|
@db.set "event-params:#{ emId }:#{ userId }", encrypt(data),
|
|
replyHandler "storing event auth #{ emId }:#{ userId }"
|
|
|
|
###
|
|
Query the DB for an action module authentication token, associated with a user.
|
|
|
|
@public getEventAuth( *userId, emId, data* )
|
|
@param {String} userId
|
|
@param {String} emId
|
|
@param {function} cb
|
|
###
|
|
exports.getEventAuth = ( userId, emId, cb ) =>
|
|
log.print 'DB', "getEventAuth: #{ emId }:#{ userId }"
|
|
@db.get "event-auth:#{ emId }:#{ userId }", ( err, data ) ->
|
|
cb err, decrypt data
|
|
|
|
|
|
###
|
|
## Rules
|
|
###
|
|
|
|
###
|
|
Store a string representation of a rule in the DB.
|
|
|
|
@public storeRule( *ruleId, userId, data* )
|
|
@param {String} ruleId
|
|
@param {String} userId
|
|
@param {String} data
|
|
###
|
|
exports.storeRule = ( ruleId, userId, data ) =>
|
|
log.print 'DB', "storeRule: #{ ruleId }"
|
|
@db.sadd 'rules', "#{ ruleId }:#{ userId }", replyHandler "storing rule key \"#{ ruleId }:#{ userId }\""
|
|
@db.sadd "user-set:#{ userId }:rules", ruleId, replyHandler "storing rule key to \"user:#{ userId }:rules\""
|
|
@db.sadd "rule-set:#{ ruleId }:users", user, replyHandler "storing user key to \"rule:#{ ruleId }:users\""
|
|
@db.set "rule:#{ ruleId }:#{ userId }", data, replyHandler "storing rule \"#{ ruleId }:#{ userId }\""
|
|
|
|
###
|
|
Query the DB for a rule and pass it to the callback(err, obj) function.
|
|
|
|
@public getRule( *ruleId, cb* )
|
|
@param {String} ruleId
|
|
@param {function} cb
|
|
###
|
|
exports.getRule = ( ruleId, cb ) =>
|
|
log.print 'DB', "getRule: #{ ruleId }"
|
|
@db.get "rule:#{ ruleId }", cb
|
|
|
|
###
|
|
Fetch all rules from the database and pass them to the callback function.
|
|
|
|
@public getRules( *cb* )
|
|
@param {function} cb
|
|
###
|
|
exports.getRules = ( cb ) ->
|
|
log.print 'DB', 'Fetching all Rules'
|
|
getSetRecords 'rules', exports.getRule, cb
|
|
|
|
###
|
|
Store a user object (needs to be a flat structure).
|
|
|
|
@public storeUser( *objUser* )
|
|
@param {Object} objUser
|
|
###
|
|
exports.storeUser = ( objUser ) =>
|
|
#TODO Only store user if not already existing, or at least only then add a private key
|
|
#for his encryption. we would want to have one private key per user, right?
|
|
log.print 'DB', "storeUser: #{ objUser.username }"
|
|
if objUser and objUser.username and objUser.password
|
|
@db.sadd 'users', objUser.username, replyHandler "storing user key #{ objUser.username }"
|
|
objUser.password = hash objUser.password
|
|
@db.hmset "user:#{ objUser.username }", objUser, replyHandler "storing user properties #{ objUser.username }"
|
|
else
|
|
log.error 'DB', new Error 'username or password was missing'
|
|
|
|
###
|
|
Associate a role with a user.
|
|
|
|
@public storeUserRole( *userId, role* )
|
|
@param {String} userId
|
|
@param {String} role
|
|
###
|
|
exports.storeUserRole = ( userId, role ) =>
|
|
log.print 'DB', "storeUserRole: #{ userId }:#{ role }"
|
|
@db.sadd 'roles', role, replyHandler "adding role #{ role } to role index set"
|
|
@db.sadd "user:#{ userId }:roles", role, replyHandler "adding role #{ role } to user #{ userId }"
|
|
@db.sadd "role:#{ role }:users", userId, replyHandler "adding user #{ userId } to role #{ role }"
|
|
|
|
###
|
|
Fetch all roles of a user and pass them to the callback(err, obj)
|
|
|
|
@public getUserRoles( *userId* )
|
|
@param {String} userId
|
|
###
|
|
exports.getUserRoles = ( userId ) =>
|
|
log.print 'DB', "getUserRole: #{ userId }"
|
|
@db.get "user-roles:#{ userId }", cb
|
|
|
|
###
|
|
Fetch all users of a role and pass them to the callback(err, obj)
|
|
|
|
@public getUserRoles( *role* )
|
|
@param {String} role
|
|
###
|
|
exports.getRoleUsers = ( role ) =>
|
|
log.print 'DB', "getRoleUsers: #{ role }"
|
|
@db.get "role-users:#{ role }", cb
|
|
|
|
###
|
|
Checks the credentials and on success returns the user object to the
|
|
callback(err, obj) function. The password has to be hashed (SHA-3-512)
|
|
beforehand by the instance closest to the user that enters the password,
|
|
because we only store hashes of passwords for safety reasons.
|
|
|
|
@public loginUser( *userId, password, cb* )
|
|
@param {String} userId
|
|
@param {String} password
|
|
@param {function} cb
|
|
###
|
|
#TODO verify and test whole function
|
|
exports.loginUser = ( userId, password, cb ) =>
|
|
log.print 'DB', "User \"#{ userId }\" tries to log in"
|
|
fCheck = ( pw ) ->
|
|
( err, obj ) ->
|
|
if err
|
|
cb err
|
|
else if obj and obj.password
|
|
if pw == obj.password
|
|
log.print 'DB', "User \"#{ obj.username }\" logged in!"
|
|
cb null, obj
|
|
else
|
|
cb new Error 'Wrong credentials!'
|
|
else
|
|
cb new Error 'User not found!'
|
|
@db.hgetall "user:#{ userId }", fCheck password
|
|
|
|
#TODO implement functions required for user sessions and the rule activation
|
|
|
|
###
|
|
Shuts down the db link.
|
|
|
|
@public shutDown()
|
|
###
|
|
exports.shutDown = => @db.quit()
|