Commit 20f9ad79 authored by Victor Andritoiu's avatar Victor Andritoiu
Browse files

init

parents
{
"presets": ["es2015", "stage-2"]
}
*/node_modules*
*/tools*
*/.git*
node_modules
*.log
.DS_Store
/dist
/sdist
FROM node:8-alpine
RUN npm install pm2 -g
RUN mkdir -p /opt && mkdir -p /opt/dlake
ADD . /opt/dlake
WORKDIR /opt/dlake
RUN npm install && npm run build:dev
CMD ["pm2-docker", "index.js"]
# Weather service implementation
This service implements basic dlake services to demonstrate both service call
and service UI injection.
## Native process
When using native process execution, take care not to use already in use port.
In order to avoid that, you can start your process using a startup shell script
as following:
```bash
#!/bin/sh
export IIOS_SERVER_PORT=20013
export IIOS_NAMESPACE=ignitialio
node index.js
```
### Redis
Ignitial.io services are based on Redis for service discovery and PUB/SUB RPC
emulation.
You need to start a Redis server to proceed. For example:
```bash
docker run -d --name redis -p 6379:6379 redis
```
## Docker
Configuration file must be based on ENV variables in order to easily configure
Docker container execution.
### Build
```shell
docker build --rm --force-rm -t ignitial/dlake .
```
### Run
```shell
docker run -d -p 20013:20013 --name dlake --link redis:redis -e REDIS_HOST="redis" -e IIOS_SERVER_PORT=20013 ignitial/dlake
```
### Kill
If Docker image does not contain PM2 and node app is not started using pm2-node,
then take care when stopiing Docker container to send TERM signal to the service.
Indeed, TERM signal will allow the service to clean up and unregister from Redis
discovery dictionary.
```shell
docker exec dlake pkill -TERM node
```
<template>
<div :id="id" class="dlake-layout">
</div>
</template>
<script>
export default {
props: [ ],
data: () => {
return {
id: 'dlake_' + Math.random().toString(36).slice(2)
}
},
computed: {
},
methods: {
},
mounted() {
},
beforeDestroy() {
}
}
</script>
<style>
.dlake-layout {
}
</style>
import DLake from './components/DLake.vue'
// Warning: component name must be globally unique in your host app
Vue.component('dlake', DLake);
Put here 3rd party libs.
// HTTP server configuration
// -----------------------------------------------------------------------------
const IIOS_SERVER_PORT = process.env.NODE_ENV === 'production' ?
8080 :
process.env.IIOS_SERVER_PORT || 4093
const IIOS_SERVER_HOST = process.env.IIOS_SERVER_HOST || '127.0.0.1'
const IIOS_SERVER_PATH_TO_SERVE = process.env.IIOS_SERVER_PATH_TO_SERVE || './dist'
// REDIS configuration
// -----------------------------------------------------------------------------
const REDIS_HOST = process.env.REDIS_HOST || '127.0.0.1'
const REDIS_PORT = process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 6379
const REDIS_DB = process.env.REDIS_DB || 0
// HTTP REST API key and context for restricted access
// -----------------------------------------------------------------------------
const REST_API_KEY = process.env.REST_API_KEY || '2bpukqziosbejet2k9duvpadajsfrm4u'
const REST_API_CONTEXT = process.env.REST_API_CONTEXT || '/api'
// Main configuration structure
// -----------------------------------------------------------------------------
module.exports = {
/* service namesapce */
namespace: process.env.IIOS_NAMESPACE || 'ignitialio',
/* redis server connection */
redis: {
host: REDIS_HOST,
port: REDIS_PORT,
db: REDIS_DB
},
/* db engine */
db: {
engine: 'mongo'
},
/* mongodb */
mongo: {
uri: process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017',
dbName: process.env.MONGODB_DBNAME || 'ignitialio',
options: process.env.MONGODB_OPTIONS,
maxAttempts: process.env.MONGODB_CONN_MAX_ATTEMPTS || 30
},
/* HTTP server declaration */
server: {
/* server host */
host: IIOS_SERVER_HOST,
/* server port */
port: IIOS_SERVER_PORT,
/* path to statically serve (at least one asset for icons for example) */
path: IIOS_SERVER_PATH_TO_SERVE
},
/* see connect-rest */
rest: {
context: REST_API_CONTEXT,
apiKeys: [ REST_API_KEY ]
},
/* options published through discovery mechanism */
publicOptions: {
/* Ignitial.io Web app access rights */
accessRights: {
owner: 'admin',
group: 'admin',
access: {
owner: 'rw',
group: 'rw',
all: 'rw'
}
},
/* declares component injection */
uiComponentInjection: true,
/* service description */
description: {
/* service icon */
icon: 'assets/dlake-64.png',
/* Internationalization: see Ignitial.io Web App */
i18n: {
'My amazing component': [ 'Mon super composant' ],
'Provides uber crazy services': [
'Fournit des services super hyper dingues'
]
},
/* eventually any other data */
title: 'My amazing component',
info: 'Provides uber crazy services'
},
/* domain related public options: could be any JSON object*/
myPublicOption: {
name: 'dlake',
description: 'Provides uber crazy services',
icon: 'assets/dlake.png',
component: 'dlake'
}
}
}
const pino = require('./lib/utils').pino
const Service = require('@ignitial/iio-services').Service
const config = require('./config')
const Mongo = require('./lib/mongo').Mongo
const Data = require('./lib/data').Data
class DLake extends Service {
constructor(options) {
// set service name before calling super
options.name = 'dlake'
super(options)
// prepare for SIGINT in Docker => must catch signal to clean up Redis dico
process.on('SIGINT', () => {
console.log('SIGINT')
this._destroy()
setTimeout(() => {
console.log('died on SIGINT')
process.exit()
}, 5000)
})
process.on('SIGTERM', () => {
console.log('SIGTERM')
this._destroy()
setTimeout(() => {
console.log('died on SIGTERM')
process.exit()
}, 500)
})
// datum references
this.datum = {}
// after $app injectio into prototype, we can load datum(s)
let datums = require('./lib/datum')
// load datum instances
for (let file of datums) {
this._addDatum(file)
}
// inject itself
Data.prototype.$app = this
this._mongo = new Mongo(this._options.mongo)
this._data = new Data(this._options.data)
// wait for service to be ready in order to initialize it
this._waitForPropertySet('_ready', true).then(() => {
// initialize the service (register into Redis dico)
this._init().then(() => {
pino.info('Service ' + this._name + ' declaration done')
}).catch(err => {
pino.error('Initialization failed >', err)
process.exit(1)
})
}).catch(err => {
pino.error('Service not ready on time >', err)
process.exit(1)
})
}
_addDatum(file) {
// datum engin option
let options = {
dbEngine: this._options.db.engine
}
let m = require(file)
for (let exported in m) {
// inject datum reference for cross uses
m[exported].prototype.$datum = this.datum
// inject reference to app
m[exported].prototype.$app = this
let instance = new m[exported](options)
this.datum[instance._name] = instance
pino.info({ file: file, options: options }, 'datum [%s] loaded', instance._name)
}
}
// provides some services here
oneServiceMethod(args) {
return new Promise((resolve, reject) => {
resolve({ somedata: 'some value' })
})
}
}
// instantiate service with its configuration
let migrant = new DLake(config)
const path = require('path')
exports.Item = (() => {
let dbEngine = require(path.join(process.cwd(), 'config')).db.engine
return require('../db/item-' + dbEngine).Item
})()
const pino = require('./utils').pino
const getAllMethods = require('@ignitial/iio-services').getMethods
class Data {
constructor(options) {
this._options = options
// name for future generic implementation
this._name = 'data'
// reference to datum models instances (singletons)
this._datum = this.$app.datum
// methods bridge by datum service for datum model instance
this._bridgedMethods = {} // list
this._services = {} // effective
// collections used for meta-control (system collections)
this._users = this._datum._users
this._accessrights = this._datum._accessrights
this._groups = this._datum._groups
this._admins = this._datum._admins
// creates bridged methods that reference datum ones
for (let d in this._datum) {
// REST service way
/* this.$app._rest.get('/' + d + '/:id', async (request, content, callback) => {
try {
await this.$app._checkRESTAccess(request.parameters.token)
let result = await this._datum[d]
.get({ _id: request.parameters.id }, request.parameters.userId)
return callback(null, result)
} catch (err) {
return callback(err, 'error')
}
}) */
// WS service way
// which are the available methods
this._bridgedMethods[d] = getAllMethods(this._datum[d])
// effective method references
this._services[d] = {}
// for each, creates a bridge with access control
for (let method of this._bridgedMethods[d]) {
this._services[d][method] = (args, userId, logged) => {
return new Promise(async (resolve, reject) => {
let accessResult = await this._checkAccess(userId, d, logged)
// writes/updates something
let lookup = this._datum[d][method].toString().match(/@_(.*?)_/)
let callType // 'read', 'write', 'exec'
if (!!lookup) {
callType = lookup[1].toLowerCase()
}
if (callType && callType.match('write')) {
// needs write access or write its own data
if (accessResult.granted.match('w') || args._id === userId) {
// userId eventually used further
this._datum[d][method](args, userId).then(datumResult => {
// normalize response
resolve(datumResult._updated)
}).catch(err => {
pino.error({ err: err },
'writing to collection [%s] method [%s] call failed for user [%s]',
d, method, userId)
reject(err)
})
} else {
pino.error(
'write access not granted for user [%s] and datum [%s]',
userId, d)
reject(new Error('write access not granted'))
}
// ... read only
} else if (callType && callType.match('read')) {
// userId eventually used further
this._datum[d][method](args, userId).then(datumResult => {
// needs at least read rights (not any more => or read its own data)
if (accessResult.granted.match('r') || (datumResult && datumResult._id === userId)) {
resolve(datumResult)
} else {
pino.error('read access not granted for user [%s] and datum [%s]', userId, d)
reject(new Error('read access not granted'))
}
}).catch(err => {
pino.error(err,
'reading from collection [%s] method [%s] call failed for user [%s]',
d, method, userId)
reject(err)
})
// something else ('x' right)
} else {
if (accessResult.granted.match('x')) {
if (this._datum[d] && this._datum[d][method]) {
try {
this._datum[d][method](args, userId).then(datumResult => {
resolve(datumResult)
}).catch(err => {
pino.error(err, 'collection [%s] method [%s] call failed for user [%s]', d, method, userId)
reject(err)
})
} catch (err) {
pino.error(err, 'method [%s] call failed with exception', method)
reject(err)
}
} else {
pino.error('method [%s] does not exist on datum [%s]', method, d)
reject(new Error('method [' + method +
'] does not exist on datum [' + d + ']'))
}
} else {
pino.error('exec access not granted for user [%s] and datum [%s]', userId, d)
reject(new Error('exec access not granted'))
}
}
})
}
}
}
pino.info('Module [%s] initialized', this._name)
}
async _checkAccess(userId, collection, logged) {
let accessGranted = ''
try {
if (userId) {
let users = await this._users.ready()
let user = await users.get({ _id: userId })
if (user) {
// logged user must match with requester
if (!user.anonymous && logged !== user.username) {
// get rights for collection
let colRights = await this._accessrights.get({ name: collection })
return { granted: colRights.access.all } // anonymous access
}
await this._admins.ready()
let isAdmin = await this._admins.get({ username: user.username })
// is super user
if (isAdmin) {
accessGranted += 'rwx'
pino.info(isAdmin, 'collection [%s] access granted as super user', collection, accessGranted)
// unless...
} else {
await this._groups.ready()
await this._accessrights.ready()
// get rights for collection
let colRights = await this._accessrights.get({ name: collection })
// get groups for user
let rqGroup = await this._groups.get({ username: user.username })
// user is collection's owner
if (colRights.owner === user.username) {
// owners access rights
accessGranted += colRights.access.owner
pino.info('collection [%s] access granted as owner: [%s]',
collection, accessGranted)
}
// get group rights if collection's group matches requester's groups
if (rqGroup && rqGroup.membership.indexOf(colRights.group) !== -1) {
// owners access rights
accessGranted += colRights.access.group
pino.info('collection [%s] access granted as group member: [%s]',
collection, accessGranted)
}
accessGranted += colRights.access.all
pino.info('collection [%s] access granted as anonymous: [%s]',
collection, accessGranted)
} // not super user
} else {
pino.error('user retrieval failed for user [%s]', userId)
}
} else {
await this._accessrights.ready()
let colRights = await this._accessrights.get({ name: collection })
pino.info('collection [%s] access granted as anonymous: [%s]', collection, colRights.access.all)
accessGranted += colRights.access.all
}
} catch (err) {
pino.error(err, 'check access error')
}
pino.info('collection [%s] access granted: [%s]', collection, accessGranted)
return { granted: accessGranted }
}
_register() {
if (this.$app.rootServices[this._name]) {
pino.warn('data service already registered')
return
}
this.$app.ws.on('module:event', event => {
if (!!event.topic.match(/module:data:?.*:request/)) {
pino.info('data request', event)
let datum = event.topic.split(':')[2]
let topic = 'module:' + this._name + ':' + datum + ':' + event.method +
':' + event.token
// injects userid for authorization check as per user's roles
// injects logged info
let loggedUser = this.$app.ws.clients[event.source].socket._logged
// injects userid for authorization check as per user's roles and call
// service method
this._services[datum][event.method](event.args, event.userId, loggedUser)
.then(result => {
pino.info('[%s] -> response [%s] - user [%s]',
this._name, topic, event.userId)
this.$app.ws.clients[event.source].socket.emit(topic, {
result: result
})
}).catch(err => {
this.$app.ws.clients[event.source].socket.emit(topic, { err: err + '' })
pino.error(err, 'data service method ' + event.method + ' call failed')
})
}
})
// defines sub-services information
let subs = {}
for (let d in this._datum) {
subs[d] = this._bridgedMethods[d]
}
// service discovery: add info to rootService table that can be obtained
// from client side
this.$app.rootServices[this._name] = {
name: this._name,
methods: null, // means that only sub-services methods are available
subs: subs // sub-services declaration
}
}
}
exports.Data = Data
const Item = require('../core/item').Item
/*
* {
* name: <string>,
* owner: <string>,
* group: <string>,
* access: {
* owner: 'r' | 'w' | 'x' | 'rw' | 'rx' | 'wx' | 'rwx' | '',
* group: 'r' | 'w' | 'x' | 'rw' | 'rx' | 'wx' | 'rwx' | '',
* all: 'r' | 'w' | 'x' | 'rw' | 'rx' | 'wx' | 'rwx' | '',
* }
* }
*/
class AccessRight extends Item {
constructor(options) {
options.name = '_accessrights'
super(options)
this.ready().then(() => {