├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── app ├── api.yml ├── config │ ├── index.js │ └── schema.json ├── express-app.js ├── index.js ├── lib │ ├── crypto.js │ ├── json-schema-config.js │ ├── prisma-cache │ │ └── src │ │ │ ├── cache-first-strategy.js │ │ │ ├── db-model.js │ │ │ └── utils.js │ ├── prisma.js │ ├── redis.js │ ├── serve-spa.js │ └── utils.js ├── log │ └── index.js ├── models │ ├── IApplication.js │ ├── IApplicationNetworkTypeLink.js │ ├── ICompany.js │ ├── ICompanyNetworkTypeLink.js │ ├── IDevice.js │ ├── IDeviceNetworkTypeLink.js │ ├── IDeviceProfile.js │ ├── IEmail.js │ ├── INetwork.js │ ├── INetworkProtocol.js │ ├── INetworkProvider.js │ ├── INetworkType.js │ ├── IPasswordPolicy.js │ ├── IProtocolData.js │ ├── IReportingProtocol.js │ ├── IUser.js │ ├── ModelAPI.js │ ├── session.js │ └── sessionManager.js ├── networkProtocols │ ├── NetworkProtocol.js │ ├── RestClient.js │ ├── handlers │ │ ├── IP │ │ │ ├── index.js │ │ │ └── metadata.js │ │ ├── LoraOpenSource │ │ │ ├── LoraOpenSource.js │ │ │ ├── LoraOpenSourceRestClient.js │ │ │ ├── v1 │ │ │ │ ├── client.js │ │ │ │ ├── index.js │ │ │ │ └── metadata.js │ │ │ └── v2 │ │ │ │ ├── client.js │ │ │ │ ├── index.js │ │ │ │ └── metadata.js │ │ ├── Loriot │ │ │ ├── Loriot.js │ │ │ ├── LoriotRestClient.js │ │ │ └── v4 │ │ │ │ ├── client.js │ │ │ │ ├── index.js │ │ │ │ └── metadata.js │ │ ├── README.md │ │ └── TheThingsNetwork │ │ │ ├── TtnRestClient.js │ │ │ └── v2 │ │ │ ├── client.js │ │ │ ├── index.js │ │ │ └── metadata.js │ ├── networkProtocolDataAccess.js │ ├── networkProtocols.js │ ├── networkTypeApi.js │ └── register.js ├── reportingProtocols │ └── postHandler.js ├── rest-server.js └── rest │ ├── restApplicationNetworkTypeLinks.js │ ├── restApplications.js │ ├── restCompanies.js │ ├── restCompanyNetworkTypeLinks.js │ ├── restDeviceNetworkTypeLinks.js │ ├── restDeviceProfiles.js │ ├── restDevices.js │ ├── restNetworkProtocols.js │ ├── restNetworkProviders.js │ ├── restNetworkProvisioningFields.js │ ├── restNetworkTypes.js │ ├── restNetworks.js │ ├── restPasswordPolicies.js │ ├── restReportingProtocols.js │ ├── restServer.js │ ├── restSessions.js │ └── restUsers.js ├── bin ├── build-ui ├── clean.js ├── demo ├── demo-seed.js ├── lib │ └── package.js ├── package.js └── release.js ├── component.json ├── development ├── Dockerfile ├── bin │ ├── generate-development-certificates │ ├── manage-db │ └── run ├── cert-conf │ ├── ca.cnf │ ├── client-catm1.cnf │ ├── client-nbiot.cnf │ ├── client.ext │ ├── server.cnf │ └── server.ext ├── chirpstack │ ├── configuration │ │ ├── lora-app-server │ │ │ ├── certs │ │ │ │ ├── http-key.pem │ │ │ │ └── http.pem │ │ │ └── lora-app-server.toml │ │ ├── lora-nwk-server │ │ │ └── loraserver.toml │ │ ├── mosquitto │ │ │ └── mosquitto.conf │ │ └── postgresql │ │ │ └── initdb │ │ │ ├── 001-init-chirpstack_ns.sh │ │ │ ├── 002-init-chirpstack_as.sh │ │ │ ├── 003-chirpstack_as_trgm.sh │ │ │ ├── 004-chirpstack_as_hstore.sh │ │ │ ├── 005-init-loraserver_ns.sh │ │ │ ├── 006-init-loraserver_as.sh │ │ │ └── 007-loraserver_as_trgm.sh │ └── docker-compose.yml ├── config.json ├── data │ └── json │ │ ├── lora1.json │ │ ├── lora2.json │ │ ├── loriot.json │ │ ├── normalized.json │ │ └── ttn.json ├── databases │ └── docker-compose.yml └── docker-compose.yml ├── docker-compose.yml ├── docs ├── RELEASE.md ├── extensions │ ├── existing.md │ ├── network-type.md │ └── overview.md ├── guides │ └── getting-started.md ├── install │ ├── build.md │ ├── configuration.md │ ├── deployment.md │ ├── download.md │ ├── requirements.md │ └── run.md ├── openapi │ ├── api.yml │ ├── dist │ │ └── api.yml │ └── endpoints │ │ ├── application-network-type-link.yml │ │ ├── application.yml │ │ ├── company-network-type-link.yml │ │ ├── company.yml │ │ ├── device-network-type-link.yml │ │ ├── device-profile.yml │ │ ├── device.yml │ │ ├── network-protocol.yml │ │ ├── network-provider.yml │ │ ├── network-type.yml │ │ ├── network.yml │ │ ├── password-policy.yml │ │ ├── reporting-protocol.yml │ │ ├── session.yml │ │ └── user.yml ├── overview │ ├── architecture.md │ ├── features.md │ └── overview.md ├── ui │ ├── devices.md │ ├── networks.md │ └── users.md └── use-cases │ ├── authenticate.md │ ├── create-network.md │ ├── create-remote-application.md │ ├── create-remote-device-profile.md │ ├── create-remote-device.md │ ├── multi-type-devices.md │ ├── pull-application.md │ ├── pull-applications.md │ ├── pull-device-profile.md │ ├── pull-device-profiles.md │ ├── pull-device.md │ ├── pull-devices.md │ ├── pull-network.md │ ├── push-application.md │ ├── push-applications.md │ ├── push-device-profile.md │ ├── push-device-profiles.md │ ├── push-device.md │ ├── push-devices.md │ ├── push-network.md │ ├── push-networks.md │ ├── start-application.md │ ├── transfer-lora-network.md │ ├── update-remote-application.md │ ├── update-remote-device-profile.md │ └── update-remote-device.md ├── lib └── seeder │ └── index.js ├── package-lock.json ├── package.json ├── prisma ├── lib │ ├── cache-instropection-query.js │ ├── post-deploy-tasks.js │ └── seed-util.js ├── prisma.yml └── versions │ └── v1 │ ├── datamodel.prisma │ └── seed.js ├── service_scripts └── lpwanserver-rest.service ├── setup-ubuntu.sh └── test ├── api ├── Dockerfile ├── docker-compose.yml ├── mocha.opts ├── run └── tests │ ├── 001-restSessions.js │ ├── 003-restUsers.js │ ├── 004-restPasswordPolicies.js │ ├── 005-restNetworkProtocols.js │ ├── 006-restNetworkProviders.js │ ├── 007-restNetworks.js │ ├── 008-restApplications.js │ ├── 011-restDeviceProfiles.js │ ├── 012-restDevices.js │ ├── 013-restApplicationNetworkTypeLinks.js │ ├── 014-restDeviceNetworkTypeLinks.js │ ├── 015-launchApplications.js │ └── 016-restReportingProtocols.js ├── data └── index.js ├── e2e-https ├── Dockerfile ├── clients │ ├── lora-server1.js │ ├── lora-server2.js │ └── lpwan.js ├── docker-compose.yml ├── mocha.opts ├── run ├── setup.js └── tests │ ├── device-import │ ├── setup.js │ └── test.spec.js │ ├── ip-device-messaging │ ├── setup.js │ └── test.spec.js │ ├── multi-type-devices │ └── test.spec.js │ └── transfer-lora-network │ ├── setup.js │ └── test.spec.js ├── e2e ├── Dockerfile ├── docker-compose.yml ├── mocha.opts ├── run ├── setup.js └── tests │ ├── 001-multi-networks.js │ ├── 002-add-device-to-existing-app.js │ ├── 003-create-app.js │ ├── 004-update-app.js │ ├── 005-update-device.js │ ├── 006-delete-device.js │ ├── 007-delete-app.js │ ├── 008-device-messaging.js │ └── 100-network-svr-cleanup.js ├── lib ├── axios-rest-client │ └── index.js ├── helpers.js ├── rc-server.js ├── rc-server │ ├── Dockerfile │ ├── index.js │ ├── package-lock.json │ └── package.json └── rest-client │ ├── README.md │ └── index.js ├── networks ├── lora-v1.js ├── lora-v2.js ├── loriot.js └── ttn.js └── unit ├── Dockerfile ├── docker-compose.yml ├── mocha.opts ├── mock └── ModelAPI-mock.js ├── run ├── setup.js └── tests ├── lib └── crypto-test.js ├── models ├── IApplication-test.js ├── IDevice-test.js ├── IDeviceProfile-test.js ├── INetwork-test.js └── IUser-test.js └── reportingProtocols └── postHandler-test.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | node_modules 5 | out 6 | .idea 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "brace-style": [ 5 | "error", 6 | "stroustrup" 7 | ], 8 | "no-unused-vars": [ 9 | "error", 10 | { 11 | "varsIgnorePattern": "should|expect|assert" 12 | } 13 | ] 14 | }, 15 | "env": { 16 | "mocha": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #SSL Certs 2 | ssl/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | # *.seed 12 | # *.sqlite3 13 | data/deviceSchemas 14 | sessions 15 | .nyc_output 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Mac OS X 21 | .DS_Store 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directory 36 | # https://docs.npmjs.com/cli/shrinkwrap#caveats 37 | node_modules 38 | 39 | # Debug log from npm 40 | npm-debug.log 41 | 42 | # Repo specific test files 43 | 44 | cb02.json 45 | b146.json 46 | sensorTest 47 | b146add.json 48 | 49 | .idea/ 50 | *.idea 51 | lpwanserver.iml 52 | 53 | key.pem 54 | privkey.pem 55 | server.crt 56 | server.pem 57 | local 58 | 59 | credentials 60 | secrets 61 | 62 | certs 63 | !development/chirpstack/configuration/lora-app-server/certs 64 | 65 | development/credentials 66 | 67 | # Generated app files 68 | app/generated 69 | 70 | # docs website 71 | website 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | node_js: 4 | - "10.15.3" 5 | services: 6 | - docker 7 | env: 8 | - DOCKER_COMPOSE_VERSION=1.23.2 9 | before_install: 10 | - sudo rm /usr/local/bin/docker-compose 11 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 12 | - chmod +x docker-compose 13 | - sudo mv docker-compose /usr/local/bin 14 | install: 15 | - npm ci 16 | - mkdir certs && ./development/bin/generate-development-certificates 17 | cache: 18 | directories: 19 | - "$HOME/.npm" 20 | script: 21 | - ./test/e2e/run 22 | # after_success: npm run coverage 23 | before_deploy: 24 | - cd .. && git clone --depth 1 https://github.com/cablelabs/lpwanserver-web-client.git && cd lpwanserver-web-client && npm ci && cd ../lpwanserver 25 | - ./bin/build-ui 26 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 27 | - npm run package 28 | deploy: 29 | provider: script 30 | script: docker push lpwanserver/lpwanserver 31 | on: 32 | branch: master 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15 2 | 3 | # set working directory 4 | WORKDIR /usr/src 5 | 6 | # Copy project files 7 | COPY app app 8 | COPY package*.json ./ 9 | 10 | RUN npm install --production 11 | 12 | EXPOSE 3200 13 | 14 | CMD [ "node", "app/index.js" ] 15 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What does this PR do? 2 | #### Do you have any concerns with this PR? 3 | #### How can the reviewer verify this PR? 4 | #### Any background context you want to provide? 5 | #### Screenshots or logs (if appropriate) 6 | #### Questions: 7 | - Have you connected this PR to the issue it resolves? 8 | - Does the documentation need an update? 9 | - Does this add new dependencies? 10 | - Have you added unit or functional tests for this PR? 11 | -------------------------------------------------------------------------------- /app/config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const Ajv = require('ajv') 3 | const schema = require('./schema.json') 4 | const { buildConfig } = require('../lib/json-schema-config') 5 | const fs = require('fs') 6 | 7 | function normalizeFilePath (x) { 8 | const filePath = (!x || x.charAt(0) === '/') ? x : path.join(__dirname, '..', x) 9 | return filePath.charAt(filePath.length - 1) === '/' ? filePath.slice(0, -1) : filePath 10 | } 11 | 12 | function readAndParseConfigFile (filePath) { 13 | if (!filePath) return {} 14 | filePath = normalizeFilePath(filePath) 15 | return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf8' })) 16 | } 17 | 18 | function validateConfig (data) { 19 | const ajv = new Ajv() 20 | const valid = ajv.validate(schema, data) 21 | if (!valid) { 22 | const error = new Error('The configuration object failed validation.') 23 | error.errors = ajv.errors 24 | throw error 25 | } 26 | } 27 | 28 | const config = buildConfig({ 29 | schema, 30 | data: readAndParseConfigFile(process.env.config_file) 31 | }) 32 | 33 | // Normalize file paths 34 | const fileRegEx = /_(file|dir)/ 35 | const filePaths = Object.keys(config).filter(x => fileRegEx.test(x)) 36 | filePaths.forEach(x => { 37 | config[x] = normalizeFilePath(config[x]) 38 | }) 39 | 40 | // Validate config 41 | validateConfig(config) 42 | 43 | // add prisma_url to the environment 44 | process.env.prisma_url = config.prisma_url 45 | 46 | module.exports = config 47 | -------------------------------------------------------------------------------- /app/express-app.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const { logger } = require('./log') 3 | const express = require('express') 4 | const morgan = require('morgan') 5 | const cors = require('cors') 6 | const cookieParser = require('cookie-parser') 7 | const RestServer = require('./rest/restServer') 8 | const serveSpa = require('./lib/serve-spa') 9 | 10 | function buildCorsMiddlewareOptions () { 11 | var whitelist = config.cors_whitelist.map(x => new RegExp(x)) 12 | return { 13 | origin (origin, callback) { 14 | if (whitelist.some(x => x.test(origin))) { 15 | callback(null, true) 16 | return 17 | } 18 | callback(new Error('Not allowed by CORS settings')) 19 | } 20 | } 21 | } 22 | 23 | async function createApp () { 24 | // Create the REST application. 25 | var app = express() 26 | 27 | if (config.public_dir) { 28 | try { 29 | serveSpa({ 30 | app, 31 | public: config.public_dir, 32 | omit: req => /^\/api/.test(req.path) 33 | }) 34 | } 35 | catch (err) { 36 | logger.error('Failed to add Express middleware to serve UI. Set public_dir to empty string to avoid error.', err) 37 | throw err 38 | } 39 | } 40 | 41 | // stream morgan to winston 42 | app.use(morgan('tiny', { stream: { write: x => logger.info(x.trim()) } })) 43 | 44 | // Add the body parser. 45 | app.use(express.json()) 46 | 47 | // Add a cookie parser. 48 | app.use(cookieParser()) 49 | 50 | app.use(cors(buildCorsMiddlewareOptions(config))) 51 | 52 | // Initialize the application support interfaces. We pass in the 53 | // application so we can add functions and API endpoints. 54 | var restServer = new RestServer(app) 55 | await restServer.initialize() 56 | 57 | return app 58 | } 59 | 60 | module.exports = { 61 | createApp 62 | } 63 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const { logger } = require('./log') 3 | const { createApp } = require('./express-app') 4 | const { createRestServer } = require('./rest-server') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | // uncaughtExceptions 9 | // uncaughtExceptions are handled and logged by winston 10 | 11 | // Log exit code to console 12 | process.on('exit', (code) => { 13 | logger.info(`LPWAN Server to exit with code: ${code}`, { exitCode: code }) 14 | }) 15 | 16 | process.on('warning', warning => { 17 | logger.warn(warning.name, warning) 18 | }) 19 | 20 | async function main () { 21 | // ensure api.yml was copied in from docs/dist 22 | fs.accessSync(path.join(__dirname, 'api.yml')) 23 | 24 | const app = await createApp() 25 | const restServer = createRestServer(app, config) 26 | 27 | const shutdown = (staticMeta = {}) => (dynamicMeta = {}) => { 28 | logger.info(`LPWAN to shutdown.`, { ...staticMeta, ...dynamicMeta }) 29 | restServer.close(() => { 30 | process.exit() 31 | }) 32 | } 33 | 34 | process.on('SIGTERM', shutdown({ signal: 'SIGTERM' })) 35 | process.on('SIGINT', shutdown({ signal: 'SIGINT' })) 36 | 37 | restServer.on('error', err => { 38 | logger.error('REST server error.', { ...err, message: err.message }) 39 | shutdown()({ error: err.message }) 40 | }) 41 | 42 | restServer.listen(config.port, () => { 43 | logger.info(`Listening on ${config.port}`) 44 | }) 45 | } 46 | 47 | main().catch(err => { 48 | logger.log({ ...err, message: `${err}`, level: 'error' }) 49 | }) 50 | -------------------------------------------------------------------------------- /app/lib/json-schema-config.js: -------------------------------------------------------------------------------- 1 | const R = require('ramda') 2 | 3 | // using the keys from the config schema, build an 4 | // object using values from process.env 5 | function buildEnvironmentVariablesConfig (schema) { 6 | const trueRx = /^true$/i 7 | const propertyKeys = Object.keys(schema.properties) 8 | let env = R.pick(propertyKeys, process.env) 9 | return Object.keys(env).reduce((acc, x) => { 10 | const { type } = schema.properties[x] 11 | switch (type.toLowerCase()) { 12 | case 'integer': return R.assoc(x, parseInt(env[x], 10), acc) 13 | case 'boolean': return R.assoc(x, trueRx.test(env[x]), acc) 14 | case 'array': return R.assoc(x, JSON.parse(env[x]), acc) 15 | default: return R.assoc(x, env[x], acc) 16 | } 17 | }, {}) 18 | } 19 | 20 | // build an object using values from the defaults in the schema 21 | function buildConfigDefaults (schema, definitions) { 22 | return Object.keys(schema.properties).reduce((acc, prop) => { 23 | let spec = schema.properties[prop] 24 | if (spec.$ref) { 25 | spec = definitions[spec.$ref.replace('#/definitions/', '')] 26 | return R.assoc(prop, buildConfigDefaults(spec, definitions), acc) 27 | } 28 | return R.assoc(prop, spec.default, acc) 29 | }, {}) 30 | } 31 | 32 | function buildConfig ({ schema, data = {} }) { 33 | return R.merge( 34 | R.mergeDeepRight( 35 | buildConfigDefaults(schema, schema.definitions), 36 | data 37 | ), 38 | buildEnvironmentVariablesConfig(schema) 39 | ) 40 | } 41 | 42 | module.exports = { 43 | buildConfig 44 | } 45 | -------------------------------------------------------------------------------- /app/lib/prisma-cache/src/cache-first-strategy.js: -------------------------------------------------------------------------------- 1 | const ObjectHash = require('object-hash') 2 | const DbModel = require('./db-model') 3 | const { mkError } = require('./utils') 4 | 5 | module.exports = class CacheFirstStrategy extends DbModel { 6 | constructor (opts) { 7 | super(opts) 8 | this.redis = opts.redis 9 | } 10 | 11 | key (...args) { 12 | return `${this.lowerName}:${args.join(':')}` 13 | } 14 | 15 | async cacheRecord (record, args) { 16 | try { 17 | let hash = ObjectHash.sha1(args) 18 | await this.redis.saddAsync(this.key('loadIndex', record.id), hash) 19 | await this.redis.setAsync(this.key('load', hash), JSON.stringify(record)) 20 | } 21 | catch (err) { 22 | this.log(`Failed to cache ${this.name} record: ${err}`) 23 | this.log(JSON.stringify({ record, args })) 24 | } 25 | } 26 | 27 | async removeFromCache (uniqueKeyObj) { 28 | let scanAsync = async (id, start) => { 29 | const result = await this.redis.sscanAsync(this.key('loadIndex', id), start) 30 | return [ parseInt(result[0], 10), ...result.slice(1) ] 31 | } 32 | try { 33 | const { id } = await this.resolveId(uniqueKeyObj) 34 | let scan = await scanAsync(id, 0) 35 | let hashes = scan[1] 36 | while (scan[0]) { 37 | scan = await scanAsync(id, scan[0]) 38 | hashes = [ ...hashes, ...scan[1] ] 39 | } 40 | let keys = hashes.map(hash => this.key('load', hash)) 41 | await Promise.all([ 42 | this.redis.del(this.key('loadIndex', id)), 43 | ...keys.map(x => this.redis.del(x)) 44 | ]) 45 | } 46 | catch (err) { 47 | this.log(`Unable to remove ${this.lowerName} cache for: ${JSON.stringify(uniqueKeyObj)}`) 48 | throw mkError(500, err) 49 | } 50 | } 51 | 52 | async resolveId (uniqueKeyObj) { 53 | if (uniqueKeyObj.id) return uniqueKeyObj 54 | return super.load(uniqueKeyObj) 55 | } 56 | 57 | async load (...args) { 58 | const hash = ObjectHash.sha1(args) 59 | let record = await this.redis.getAsync(this.key('load', hash)) 60 | if (record) return JSON.parse(record) 61 | record = await super.load(...args) 62 | await this.cacheRecord(record, args) 63 | return record 64 | } 65 | 66 | async update (uniqueKeyObj, data, opts = {}) { 67 | await this.removeFromCache(uniqueKeyObj) 68 | return super.update(uniqueKeyObj, data, opts) 69 | } 70 | 71 | async remove (uniqueKeyObj) { 72 | await this.removeFromCache(uniqueKeyObj) 73 | return super.remove(uniqueKeyObj) 74 | } 75 | 76 | async clearModelFromCache () { 77 | let scan = await this.redis.scanAsync(0) 78 | let regex = new RegExp(`^${this.lowerName}`) 79 | while (parseInt(scan[0], 10)) { 80 | let keys = scan[1].filter(x => regex.test(x)) 81 | await Promise.all(keys.map(x => this.redis.del(x))) 82 | scan = await this.redis.scanAsync(scan[0]) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/lib/prisma-cache/src/db-model.js: -------------------------------------------------------------------------------- 1 | const { 2 | onFail, 3 | lowerFirst, 4 | upperFirst, 5 | formatRelationshipsIn, 6 | formatInputData, 7 | mkError } = require('./utils') 8 | 9 | module.exports = class DbModel { 10 | constructor ({ name, pluralName, fragments, defaultFragmentKey, prisma, log }) { 11 | this.lowerName = lowerFirst(name) 12 | this.upperName = upperFirst(name) 13 | this.lowerPluralName = lowerFirst(pluralName) 14 | this.upperPluralName = upperFirst(pluralName) 15 | this.fragments = fragments 16 | this.defaultFragmentKey = defaultFragmentKey 17 | this.prisma = prisma 18 | this.log = log || console.log.bind(console) 19 | } 20 | 21 | async load (uniqueKeyObj, opts = {}) { 22 | let { prisma, lowerName, fragments } = this 23 | let fragment = opts.fragment || this.defaultFragmentKey 24 | const rec = await onFail(400, () => prisma[lowerName](uniqueKeyObj).$fragment(fragments[fragment])) 25 | if (!rec) throw mkError(404, `${this.upperName} not found.`) 26 | return rec 27 | } 28 | 29 | async list ({ limit, offset, ...where } = {}, opts = {}) { 30 | let { prisma, lowerPluralName } = this 31 | let fragment = opts.fragment || this.defaultFragmentKey 32 | where = formatRelationshipsIn(where) 33 | const query = { where } 34 | if (limit) query.first = limit 35 | if (offset) query.skip = offset 36 | let promises = [ 37 | prisma[lowerPluralName](query).$fragment(this.fragments[fragment]) 38 | ] 39 | promises.push(opts.includeTotal 40 | ? prisma[`${lowerPluralName}Connection`]({ where }).aggregate().count() 41 | : Promise.resolve() 42 | ) 43 | return Promise.all(promises) 44 | } 45 | 46 | async create (data, opts = {}) { 47 | let fragment = opts.fragment || this.defaultFragmentKey 48 | data = formatInputData(data) 49 | return this.prisma[`create${this.upperName}`](data).$fragment(this.fragments[fragment]) 50 | } 51 | 52 | async update (uniqueKeyObj, data, opts = {}) { 53 | let fragment = opts.fragment || this.defaultFragmentKey 54 | data = formatInputData(data) 55 | return this.prisma[`update${this.upperName}`]({ data, where: uniqueKeyObj }).$fragment(this.fragments[fragment]) 56 | } 57 | 58 | async remove (uniqueKeyObj) { 59 | return onFail(400, () => this.prisma[`delete${this.upperName}`](uniqueKeyObj)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/lib/prisma-cache/src/utils.js: -------------------------------------------------------------------------------- 1 | // Config access. 2 | const R = require('ramda') 3 | const { prune } = require('dead-leaves') 4 | 5 | function mkError (status, err) { 6 | if (typeof err === 'string') err = new Error(err) 7 | err.status = err.statusCode = status 8 | throw err 9 | } 10 | 11 | async function onFail (status, action) { 12 | try { 13 | const result = await action() 14 | return result 15 | } 16 | catch (err) { 17 | throw mkError(status, err) 18 | } 19 | } 20 | 21 | function lowerFirst (x) { 22 | return `${x.charAt(0).toLowerCase()}${x.slice(1)}` 23 | } 24 | 25 | function upperFirst (x) { 26 | return `${x.charAt(0).toUpperCase()}${x.slice(1)}` 27 | } 28 | 29 | function formatRelationshipsIn (data) { 30 | const REF_PROP_RE = /^(.+)(Id|ID)$/ 31 | return R.keys(data).reduce((acc, x) => { 32 | if (!REF_PROP_RE.test(x)) return R.assoc(x, data[x], acc) 33 | if (data[x] == null) return acc 34 | return R.assoc(x.replace(/(Id|ID)$/, ''), { id: data[x] }, acc) 35 | }, {}) 36 | } 37 | 38 | function isRelationshipRef (val) { 39 | return val && typeof val === 'object' && 'id' in val && R.keys(val).length === 1 40 | } 41 | 42 | function formatRelationshipsOut (data) { 43 | return R.keys(data).reduce((acc, x) => { 44 | if (isRelationshipRef(data[x])) { 45 | return R.assoc(`${x}Id`, data[x].id, acc) 46 | } 47 | return R.assoc(x, data[x], acc) 48 | }, {}) 49 | } 50 | 51 | function connectRelationshipReferences (data) { 52 | return R.keys(data).reduce((acc, x) => { 53 | if (isRelationshipRef(acc[x])) { 54 | acc[x] = { connect: acc[x] } 55 | } 56 | return acc 57 | }, data) 58 | } 59 | 60 | function formatInputPruneFilter (x) { 61 | return typeof x !== 'undefined' 62 | } 63 | 64 | // remove any undefined properties, then format the reference connections 65 | const formatInputData = R.compose( 66 | connectRelationshipReferences, 67 | formatRelationshipsIn, 68 | x => prune(x, formatInputPruneFilter) 69 | ) 70 | 71 | module.exports = { 72 | mkError, 73 | onFail, 74 | lowerFirst, 75 | upperFirst, 76 | formatRelationshipsIn, 77 | formatRelationshipsOut, 78 | connectRelationshipReferences, 79 | formatInputData 80 | } 81 | -------------------------------------------------------------------------------- /app/lib/prisma.js: -------------------------------------------------------------------------------- 1 | // Config access. 2 | const R = require('ramda') 3 | const { mutate, onFail } = require('./utils') 4 | const { prune } = require('dead-leaves') 5 | const httpError = require('http-errors') 6 | 7 | const prismaClient = require('../generated/prisma-client') 8 | 9 | function formatRelationshipsIn (data) { 10 | const REF_PROP_RE = /^(.+)Id$/ 11 | return R.keys(data).reduce((acc, x) => { 12 | if (!REF_PROP_RE.test(x)) return mutate(x, data[x], acc) 13 | if (data[x] == null) return acc 14 | return mutate(x.replace(/Id$/, ''), { id: data[x] }, acc) 15 | }, {}) 16 | } 17 | 18 | function isRelationshipRef (val) { 19 | return val && typeof val === 'object' && 'id' in val && R.keys(val).length === 1 20 | } 21 | 22 | function formatRelationshipsOut (data) { 23 | return R.keys(data).reduce((acc, x) => { 24 | if (isRelationshipRef(data[x])) { 25 | return mutate(`${x}Id`, data[x].id, acc) 26 | } 27 | return mutate(x, data[x], acc) 28 | }, {}) 29 | } 30 | 31 | function connectRelationshipReferences (data) { 32 | return R.keys(data).reduce((acc, x) => { 33 | if (isRelationshipRef(acc[x])) { 34 | acc[x] = { connect: acc[x] } 35 | } 36 | return acc 37 | }, data) 38 | } 39 | 40 | function formatInputPruneFilter (x) { 41 | return typeof x !== 'undefined' 42 | } 43 | 44 | // remove any undefined properties, then format the reference connections 45 | const formatInputData = R.compose( 46 | connectRelationshipReferences, 47 | formatRelationshipsIn, 48 | x => prune(x, formatInputPruneFilter) 49 | ) 50 | 51 | function loadRecord (modelName, fragments, defaultFragment) { 52 | const modelNameCapitolized = `${modelName.charAt(0).toUpperCase()}${modelName.slice(1)}` 53 | const modelNameLower = `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}` 54 | return async (uniqueKeyObj, fragment = defaultFragment) => { 55 | const rec = await onFail(400, () => prismaClient.prisma[modelNameLower](uniqueKeyObj).$fragment(fragments[fragment])) 56 | if (!rec) throw httpError(404, `${modelNameCapitolized} not found.`) 57 | return rec 58 | } 59 | } 60 | 61 | module.exports = { 62 | ...prismaClient, 63 | formatRelationshipsIn, 64 | formatRelationshipsOut, 65 | connectRelationshipReferences, 66 | formatInputData, 67 | loadRecord 68 | } 69 | -------------------------------------------------------------------------------- /app/lib/redis.js: -------------------------------------------------------------------------------- 1 | const config = require('../config') 2 | const redis = require('redis') 3 | const bluebird = require('bluebird') 4 | 5 | bluebird.promisifyAll(redis) 6 | 7 | module.exports = { 8 | redisClient: redis.createClient({ url: config.redis_url }), 9 | redisPub: redis.createClient({ url: config.redis_url }), 10 | redisSub: redis.createClient({ url: config.redis_url }) 11 | } 12 | -------------------------------------------------------------------------------- /app/lib/serve-spa.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const Express = require('express') 3 | const fs = require('fs') 4 | 5 | module.exports = function serveSpa ({ app, omit, public: publicDir }) { 6 | if (!omit) omit = () => false 7 | const indexFilePath = path.join(publicDir, 'index.html') 8 | 9 | // ensure server has access to index.html 10 | fs.accessSync(indexFilePath) 11 | 12 | // add middleware to express 13 | app 14 | .use(Express.static(publicDir)) 15 | .get('*', (req, res, next) => { 16 | if (req.accepts('html') && !omit(req)) { 17 | return res.sendFile(indexFilePath) 18 | } 19 | return next() 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /app/log/index.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston') 2 | const config = require('../config') 3 | 4 | const logger = createLogger({ 5 | level: config.log_level, 6 | defaultMeta: { 7 | service: 'lpwanserver' 8 | }, 9 | transports: [ 10 | new transports.Console({ 11 | level: 'debug', 12 | format: format.combine( 13 | format.colorize(), 14 | format.simple(), 15 | format.errors({ stack: true }) 16 | ), 17 | handleExceptions: true 18 | }) 19 | ] 20 | }) 21 | 22 | // Logging to files 23 | const fileTransportOpts = { 24 | format: format.combine( 25 | format.timestamp({ 26 | format: 'YYYY-MM-DD HH:mm:ss' 27 | }), 28 | format.errors({ stack: true }), 29 | format.splat(), 30 | format.json() 31 | ), 32 | handleExceptions: true 33 | } 34 | if (config.log_file) { 35 | logger.add(new transports.File({ ...fileTransportOpts, level: 'info', filename: config.log_file })) 36 | } 37 | if (config.log_file_errors) { 38 | logger.add(new transports.File({ ...fileTransportOpts, level: 'error', filename: config.log_file_errors })) 39 | } 40 | 41 | module.exports = { 42 | logger 43 | } 44 | -------------------------------------------------------------------------------- /app/models/INetworkProvider.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../lib/prisma') 2 | const httpError = require('http-errors') 3 | const { renameKeys } = require('../lib/utils') 4 | const CacheFirstStrategy = require('../lib/prisma-cache/src/cache-first-strategy') 5 | const { logger } = require('../log') 6 | const { redisClient } = require('../lib/redis') 7 | 8 | // ****************************************************************************** 9 | // Fragments for how the data should be returned from Prisma. 10 | // ****************************************************************************** 11 | const fragments = { 12 | basic: `fragment BasicNetworkProvider on NetworkProvider { 13 | id 14 | name 15 | }` 16 | } 17 | 18 | // ****************************************************************************** 19 | // Database Client 20 | // ****************************************************************************** 21 | const DB = new CacheFirstStrategy({ 22 | name: 'networkProvider', 23 | pluralName: 'networkProviders', 24 | fragments, 25 | defaultFragmentKey: 'basic', 26 | prisma, 27 | redis: redisClient, 28 | log: logger.info.bind(logger) 29 | }) 30 | 31 | // ****************************************************************************** 32 | // Helpers 33 | // ****************************************************************************** 34 | const renameQueryKeys = renameKeys({ search: 'name_contains' }) 35 | 36 | // ****************************************************************************** 37 | // Model 38 | // ****************************************************************************** 39 | module.exports = class NetworkProvider { 40 | load (id) { 41 | return DB.load({ id }) 42 | } 43 | 44 | async list (query = {}, opts) { 45 | return DB.list(renameQueryKeys(query), opts) 46 | } 47 | 48 | create (name) { 49 | return DB.create({ name }) 50 | } 51 | 52 | update ({ id, ...data }) { 53 | if (!id) throw httpError(400, 'No existing NetworkProvider ID') 54 | return DB.update({ id }, data) 55 | } 56 | 57 | remove (id) { 58 | return DB.remove({ id }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/models/INetworkType.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../lib/prisma') 2 | const httpError = require('http-errors') 3 | const { renameKeys } = require('../lib/utils') 4 | const CacheFirstStrategy = require('../lib/prisma-cache/src/cache-first-strategy') 5 | const { logger } = require('../log') 6 | const { redisClient } = require('../lib/redis') 7 | 8 | // ****************************************************************************** 9 | // Fragments for how the data should be returned from Prisma. 10 | // ****************************************************************************** 11 | const fragments = { 12 | basic: `fragment BasicNetworkType on NetworkType { 13 | id 14 | name 15 | }` 16 | } 17 | 18 | // ****************************************************************************** 19 | // Database Client 20 | // ****************************************************************************** 21 | const DB = new CacheFirstStrategy({ 22 | name: 'networkType', 23 | pluralName: 'networkTypes', 24 | fragments, 25 | defaultFragmentKey: 'basic', 26 | prisma, 27 | redis: redisClient, 28 | log: logger.info.bind(logger) 29 | }) 30 | 31 | // ****************************************************************************** 32 | // Helpers 33 | // ****************************************************************************** 34 | const renameQueryKeys = renameKeys({ search: 'name_contains' }) 35 | 36 | // ****************************************************************************** 37 | // Model 38 | // ****************************************************************************** 39 | module.exports = class NetworkType { 40 | load (id) { 41 | return DB.load({ id }) 42 | } 43 | 44 | async list (query = {}, opts) { 45 | return DB.list(renameQueryKeys(query), opts) 46 | } 47 | 48 | create (name) { 49 | return DB.create({ name }) 50 | } 51 | 52 | update ({ id, ...data }) { 53 | if (!id) throw httpError(400, 'No existing NetworkType ID') 54 | return DB.update({ id }, data) 55 | } 56 | 57 | remove (id) { 58 | return DB.remove({ id }) 59 | } 60 | 61 | loadByName (name) { 62 | return DB.load({ name }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/models/IPasswordPolicy.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../lib/prisma') 2 | const httpError = require('http-errors') 3 | const { logger } = require('../log') 4 | const CacheFirstStrategy = require('../lib/prisma-cache/src/cache-first-strategy') 5 | const { redisClient } = require('../lib/redis') 6 | 7 | // ****************************************************************************** 8 | // Fragments for how the data should be returned from Prisma. 9 | // ****************************************************************************** 10 | const fragments = { 11 | basic: `fragment BasicPasswordPolicy on PasswordPolicy { 12 | id 13 | ruleText 14 | ruleRegExp 15 | company { 16 | id 17 | } 18 | }` 19 | } 20 | 21 | // ****************************************************************************** 22 | // Database Client 23 | // ****************************************************************************** 24 | const DB = new CacheFirstStrategy({ 25 | name: 'passwordPolicy', 26 | pluralName: 'passwordPolicies', 27 | fragments, 28 | defaultFragmentKey: 'basic', 29 | prisma, 30 | redis: redisClient, 31 | log: logger.info.bind(logger) 32 | }) 33 | 34 | // ****************************************************************************** 35 | // Helpers 36 | // ****************************************************************************** 37 | function testRegExp (x) { 38 | return new RegExp(x) 39 | } 40 | 41 | // ****************************************************************************** 42 | // Model 43 | // ****************************************************************************** 44 | module.exports = class PasswordPolicy { 45 | constructor (companyModel) { 46 | this.companies = companyModel 47 | } 48 | 49 | load (id) { 50 | return DB.load({ id }) 51 | } 52 | 53 | async list (companyId, opts) { 54 | // Verify that the company exists. 55 | await this.companies.load(companyId) 56 | const where = { OR: [ 57 | { company: { id: companyId } }, 58 | { company: null } 59 | ] } 60 | return DB.list(where, opts) 61 | } 62 | 63 | async create (ruleText, ruleRegExp, companyId) { 64 | // verify regexp doesn't throw when created 65 | testRegExp(ruleRegExp) 66 | return DB.create({ ruleText, ruleRegExp, companyId }) 67 | } 68 | 69 | async update ({ id, ...data }) { 70 | if (!id) throw httpError(400, 'No existing PasswordPolicy ID') 71 | if (data.companyId) { 72 | // Verify that any new company exists if passed in. 73 | await this.companies.load(data.companyId) 74 | } 75 | return DB.update({ id }, data) 76 | } 77 | 78 | remove (id) { 79 | return DB.remove({ id }) 80 | } 81 | 82 | async validatePassword (companyId, password) { 83 | // Get the rules from the passwordPolicies table 84 | const [ pwPolicies ] = await this.list(companyId) 85 | const failed = pwPolicies.filter(x => !(new RegExp(x.ruleRegExp).test(password))) 86 | if (!failed.length) return true 87 | throw httpError(400, `Password failed these policies: ${failed.map(x => x.ruleText).join('; ')}`) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/models/IReportingProtocol.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../lib/prisma') 2 | const httpError = require('http-errors') 3 | const { renameKeys } = require('../lib/utils') 4 | const path = require('path') 5 | const { redisClient } = require('../lib/redis') 6 | const CacheFirstStrategy = require('../lib/prisma-cache/src/cache-first-strategy') 7 | const { logger } = require('../log') 8 | 9 | // ****************************************************************************** 10 | // Fragments for how the data should be returned from Prisma. 11 | // ****************************************************************************** 12 | const fragments = { 13 | basic: `fragment BasicReportingProtocol on ReportingProtocol { 14 | id 15 | name 16 | protocolHandler 17 | }` 18 | } 19 | 20 | // ****************************************************************************** 21 | // Database Client 22 | // ****************************************************************************** 23 | const DB = new CacheFirstStrategy({ 24 | name: 'reportingProtocol', 25 | pluralName: 'reportingProtocols', 26 | fragments, 27 | defaultFragmentKey: 'basic', 28 | prisma, 29 | redis: redisClient, 30 | log: logger.info.bind(logger) 31 | }) 32 | 33 | // ****************************************************************************** 34 | // Helpers 35 | // ****************************************************************************** 36 | const renameQueryKeys = renameKeys({ search: 'name_contains' }) 37 | 38 | // ****************************************************************************** 39 | // Model 40 | // ****************************************************************************** 41 | module.exports = class ReportingProtocol { 42 | constructor () { 43 | this.handlers = {} 44 | } 45 | 46 | async initialize () { 47 | const [ records ] = await DB.list() 48 | const handlersDir = path.join(__dirname, '../reportingProtocols') 49 | records.forEach(x => { 50 | let Handler = require(path.join(handlersDir, x.protocolHandler)) 51 | this.handlers[x.id] = new Handler({ reportingProtocolId: x.id }) 52 | }) 53 | } 54 | 55 | load (id) { 56 | return DB.load({ id }) 57 | } 58 | 59 | async list (query = {}, opts) { 60 | return DB.list(renameQueryKeys(query), opts) 61 | } 62 | 63 | create (name, protocolHandler) { 64 | return DB.create({ name, protocolHandler }) 65 | } 66 | 67 | update ({ id, ...data }) { 68 | if (!id) throw httpError(400, 'No existing ReportingProtocol ID') 69 | return DB.update({ id }, data) 70 | } 71 | 72 | remove (id) { 73 | return DB.remove({ id }) 74 | } 75 | 76 | getHandler (id) { 77 | return this.handlers[id] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/models/session.js: -------------------------------------------------------------------------------- 1 | //* ***************************************************************************** 2 | // The Session interface. 3 | // 4 | // The session interface keeps track of the client's resources in use, 5 | // especially their connections to remote networks 6 | //* ***************************************************************************** 7 | module.exports = class Session { 8 | constructor (token) { 9 | this.jwtToken = token 10 | this.networkSessions = [] 11 | } 12 | 13 | addConnection (name, connection, api) { 14 | this.networkSessions.push({ name, connection, api }) 15 | } 16 | 17 | async dropConnections () { 18 | await Promise.all(this.networkSessions.map(x => x.api.disconnect(x.connection))) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/networkProtocols/RestClient.js: -------------------------------------------------------------------------------- 1 | const requestClient = require('request-promise') 2 | const R = require('ramda') 3 | const { URLSearchParams } = require('url') 4 | const { joinUrl } = require('../lib/utils') 5 | const EventEmitter = require('events') 6 | 7 | module.exports = class RestClient extends EventEmitter { 8 | constructor ({ cache, logger } = {}) { 9 | super() 10 | this.cache = cache || new Map() 11 | this.logger = logger 12 | } 13 | 14 | async request ({ opts, transformResponse = R.identity }) { 15 | if (opts.json == null) opts.json = true 16 | let body 17 | try { 18 | body = await requestClient(opts) 19 | if (this.logger) { 20 | this.logger.info(`NETWORK REQUEST`, { opts }) 21 | this.logger.info(`NETWORK RESPONSE`, { body }) 22 | } 23 | } 24 | catch (err) { 25 | if (this.logger) { 26 | this.logger.error(`NETWORK ERROR`, { opts, error: err }) 27 | } 28 | throw err 29 | } 30 | return transformResponse(body) 31 | } 32 | 33 | constructUrl ({ network, url, params }) { 34 | if (params && !R.isEmpty(params)) { 35 | const qs = new URLSearchParams(params).toString() 36 | url = `${url}?${qs}` 37 | } 38 | if (network && url.indexOf('http') !== 0) { 39 | url = joinUrl(network.baseUrl, url) 40 | } 41 | // remove possible double slash 42 | return url.replace(/([^:]\/)\/+/g, '$1') 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/IP/index.js: -------------------------------------------------------------------------------- 1 | const NetworkProtocol = require('../../NetworkProtocol') 2 | const R = require('ramda') 3 | const httpError = require('http-errors') 4 | 5 | module.exports = class LoraOpenSource extends NetworkProtocol { 6 | async passDataToApplication (app, device, devEUI, data) { 7 | const reportingAPI = this.modelAPI.reportingProtocols.getHandler(app.reportingProtocol.id) 8 | data.deviceInfo = { 9 | ...R.pick(['name', 'description'], device), 10 | model: device.deviceModel, 11 | devEUI 12 | } 13 | data.applicationInfo = { name: app.name } 14 | await reportingAPI.report(data, app.baseUrl, app.name) 15 | } 16 | 17 | async passDataToDevice (devNTL, body) { 18 | if (!devNTL.networkSettings.devEUI) { 19 | throw httpError(400, `Error passing message to device ${devNTL.device.id}: no known devEUI.`) 20 | } 21 | await this.modelAPI.devices.pushIpDeviceDownlink(devNTL.networkSettings.devEUI, body) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/IP/metadata.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | } 3 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/LoraOpenSource/v1/index.js: -------------------------------------------------------------------------------- 1 | const LoraOpenSource = require('../LoraOpenSource') 2 | const ApiClient = require('./client') 3 | const R = require('ramda') 4 | const { renameKeys } = require('../../../../lib/utils') 5 | 6 | const renameAppKey = renameKeys({ appKey: 'nwkKey' }) 7 | 8 | module.exports = class LoraOpenSourceV1 extends LoraOpenSource { 9 | constructor (opts) { 10 | super(opts) 11 | this.client = new ApiClient() 12 | } 13 | 14 | buildDeviceNetworkSettings (remoteDevice) { 15 | const result = super.buildDeviceNetworkSettings(remoteDevice) 16 | if (result.deviceKeys) { 17 | result.deviceKeys = renameAppKey(result.deviceKeys) 18 | } 19 | if (result.deviceActivation) { 20 | result.deviceActivation = renameKeys({ fCntDown: 'aFCntDown', nwkSKey: 'fNwkSIntKey' }, result.deviceActivation) 21 | } 22 | return result 23 | } 24 | 25 | buildRemoteDevice (device, deviceNtl, deviceProfile, remoteAppId, remoteDeviceProfileId) { 26 | const result = super.buildRemoteDevice(device, deviceNtl, deviceProfile, remoteAppId, remoteDeviceProfileId) 27 | if (deviceNtl.networkSettings.deviceKeys) { 28 | result.deviceKeys = { 29 | devEUI: deviceNtl.networkSettings.devEUI, 30 | appKey: deviceNtl.networkSettings.deviceKeys.nwkKey 31 | } 32 | } 33 | else if (deviceNtl.networkSettings.deviceActivation && deviceProfile.networkSettings.macVersion.slice(0, 3) === '1.0') { 34 | result.deviceActivation = { 35 | devEUI: deviceNtl.networkSettings.devEUI, 36 | ...R.pick(['appSKey', 'devAddr', 'fCntUp'], deviceNtl.networkSettings.deviceActivation), 37 | fCntDwn: deviceNtl.networkSettings.deviceActivation.aFCntDown, 38 | nwkSKey: deviceNtl.networkSettings.deviceActivation.fNwkSIntKey 39 | } 40 | 41 | Object.assign(result.deviceActivation, deviceNtl.networkSettings.deviceActivation) 42 | } 43 | return result 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/LoraOpenSource/v1/metadata.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | protocolHandlerName: 'ChirpStack', 3 | version: 4 | { 5 | versionText: 'Version 1.0', 6 | versionValue: '1.0' 7 | }, 8 | networkType: 'Lora', 9 | oauthUrl: '', 10 | protocolHandlerNetworkFields: [ 11 | { 12 | name: 'username', 13 | description: 'The username of the ChirpStack admin account', 14 | help: '', 15 | type: 'string', 16 | label: 'Username', 17 | value: '', 18 | required: true, 19 | placeholder: 'myChirpStackUsername', 20 | oauthQueryParameter: '' 21 | }, 22 | { 23 | name: 'password', 24 | description: 'The password of the ChirpStack admin account', 25 | help: '', 26 | type: 'password', 27 | label: 'Password', 28 | value: '', 29 | required: true, 30 | placeholder: 'myChirpStackPassword', 31 | oauthQueryParameter: '' 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/LoraOpenSource/v2/client.js: -------------------------------------------------------------------------------- 1 | const LoraOpenSourceRestClient = require('../LoraOpenSourceRestClient') 2 | const R = require('ramda') 3 | 4 | module.exports = class LoraOpenSourceV1RestClient extends LoraOpenSourceRestClient { 5 | createOrganization (network, body) { 6 | return super.createOrganization(network, { organization: body }) 7 | } 8 | updateOrganization (network, body) { 9 | return super.updateOrganization(network, { organization: body }) 10 | } 11 | async createUser (network, body) { 12 | const props = ['organizations', 'password'] 13 | return super.createUser(network, { 14 | ...R.pick(props, body), 15 | user: R.omit(props, body) 16 | }) 17 | } 18 | createServiceProfile (network, body) { 19 | return super.createServiceProfile(network, { serviceProfile: body }) 20 | } 21 | updateServiceProfile (network, id, body) { 22 | return super.updateServiceProfile(network, id, { serviceProfile: body }) 23 | } 24 | createApplication (network, body) { 25 | return super.createApplication(network, { application: body }) 26 | } 27 | updateApplication (network, id, body) { 28 | return super.updateApplication(network, id, { application: body }) 29 | } 30 | createApplicationIntegration (network, appId, id, body) { 31 | return super.createApplicationIntegration(network, appId, id, { integration: body }) 32 | } 33 | updateApplicationIntegration (network, appId, id, body) { 34 | return super.updateApplicationIntegration(network, appId, id, { integration: body }) 35 | } 36 | createDeviceProfile (network, body) { 37 | return super.createDeviceProfile(network, { deviceProfile: body }) 38 | } 39 | updateDeviceProfile (network, id, body) { 40 | return super.updateDeviceProfile(network, id, { deviceProfile: body }) 41 | } 42 | createDevice (network, body) { 43 | return super.createDevice(network, { device: body }) 44 | } 45 | updateDevice (network, id, body) { 46 | return super.updateDevice(network, id, { device: body }) 47 | } 48 | createDeviceKeys (network, id, body) { 49 | return super.createDeviceKeys(network, id, { deviceKeys: body }) 50 | } 51 | updateDeviceKeys (network, id, body) { 52 | return super.updateDeviceKeys(network, id, { deviceKeys: body }) 53 | } 54 | activateDevice (network, id, body) { 55 | return super.activateDevice(network, id, { deviceActivation: body }) 56 | } 57 | listDevices (network, appId, params) { 58 | const opts = { url: this.constructUrl({ url: '/devices', params: { ...params, applicationID: appId } }) } 59 | return this.request(network, opts) 60 | } 61 | createDeviceMessage (network, id, body) { 62 | return super.createDeviceMessage(network, id, { deviceQueueItem: body }) 63 | } 64 | async listDeviceMessages (network, id) { 65 | const { deviceQueueItems } = await super.listDeviceMessages(network, id) 66 | return { result: deviceQueueItems } 67 | } 68 | createNetworkServer (network, body) { 69 | return super.createNetworkServer(network, { networkServer: body }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/LoraOpenSource/v2/index.js: -------------------------------------------------------------------------------- 1 | const LoraOpenSource = require('../LoraOpenSource') 2 | const ApiClient = require('./client') 3 | const R = require('ramda') 4 | 5 | module.exports = class LoraOpenSourceV2 extends LoraOpenSource { 6 | constructor (opts) { 7 | super(opts) 8 | this.client = new ApiClient() 9 | } 10 | 11 | buildRemoteDevice (device, deviceNtl, deviceProfile, remoteAppId, remoteDeviceProfileId) { 12 | const result = super.buildRemoteDevice(device, deviceNtl, deviceProfile, remoteAppId, remoteDeviceProfileId) 13 | const NS = deviceNtl.networkSettings 14 | const { deviceKeys, deviceActivation } = NS 15 | if (deviceKeys) { 16 | result.deviceKeys = { devEUI: NS.devEUI } 17 | Object.assign(result.deviceKeys, deviceKeys) 18 | if (!result.deviceKeys.nwkKey) { 19 | result.deviceKeys.nwkKey = result.deviceKeys.appKey 20 | } 21 | } 22 | else if (deviceActivation) { 23 | const mac = deviceProfile.networkSettings.macVersion.slice(0, 3) 24 | result.deviceActivation = R.merge(deviceActivation, { devEUI: NS.devEUI }) 25 | if (mac === '1.0') { 26 | result.deviceActivation.nwkSEncKey = deviceActivation.fNwkSIntKey 27 | result.deviceActivation.sNwkSIntKey = deviceActivation.fNwkSIntKey 28 | } 29 | } 30 | return result 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/LoraOpenSource/v2/metadata.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | protocolHandlerName: 'ChirpStack 2.0', 3 | version: 4 | { 5 | versionText: 'Version 2.0', 6 | versionValue: '2.0' 7 | }, 8 | networkType: 'Lora', 9 | oauthUrl: '', 10 | protocolHandlerNetworkFields: [ 11 | { 12 | name: 'username', 13 | description: 'The username of the ChirpStack admin account', 14 | help: '', 15 | type: 'string', 16 | label: 'Username', 17 | value: '', 18 | required: true, 19 | placeholder: 'myChirpStackUsername', 20 | oauthQueryParameter: '' 21 | }, 22 | { 23 | name: 'password', 24 | description: 'The password of the ChirpStack admin account', 25 | help: '', 26 | type: 'password', 27 | label: 'Password', 28 | value: '', 29 | required: true, 30 | placeholder: 'myChirpStackPassword', 31 | oauthQueryParameter: '' 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/Loriot/v4/client.js: -------------------------------------------------------------------------------- 1 | const LoriotRestClient = require('../LoriotRestClient') 2 | 3 | module.exports = class LoriotV4RestClient extends LoriotRestClient { 4 | } 5 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/Loriot/v4/index.js: -------------------------------------------------------------------------------- 1 | const Loriot = require('../Loriot') 2 | const ApiClient = require('./client') 3 | 4 | module.exports = class LoriotV4 extends Loriot { 5 | constructor (opts) { 6 | super(opts) 7 | this.client = new ApiClient() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/Loriot/v4/metadata.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | protocolHandlerName: 'Loriot', 3 | version: 4 | { 5 | versionText: 'Version 4.0', 6 | versionValue: '4.0' 7 | }, 8 | networkType: 'Lora', 9 | oauthUrl: '', 10 | protocolHandlerNetworkFields: [ 11 | { 12 | name: 'apiKey', 13 | description: 'The api key created through the Loriot console.', 14 | help: '', 15 | type: 'string', 16 | label: 'API Key', 17 | value: '', 18 | required: true, 19 | placeholder: '', 20 | oauthQueryParameter: '' 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/README.md: -------------------------------------------------------------------------------- 1 | # Network Protocol Handlers 2 | 3 | Handlers are registered alphabetically. If one protocol use's another 4 | as it's master protocol, the master protocol should be listed first. -------------------------------------------------------------------------------- /app/networkProtocols/handlers/TheThingsNetwork/v2/client.js: -------------------------------------------------------------------------------- 1 | const TtnRestClient = require('../TtnRestClient') 2 | 3 | module.exports = class TtnV2RestClient extends TtnRestClient { 4 | } 5 | -------------------------------------------------------------------------------- /app/networkProtocols/handlers/TheThingsNetwork/v2/metadata.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | protocolHandlerName: 'TheThingsNetwork', 3 | version: 4 | { 5 | versionText: 'Version 2.0', 6 | versionValue: '2.0' 7 | }, 8 | networkType: 'Lora', 9 | oauthUrl: 'https://account.thethingsnetwork.org/users/authorize', 10 | protocolHandlerNetworkFields: [ 11 | { 12 | name: 'clientId', 13 | description: 'The client id chosen when registering the LPWan', 14 | help: '', 15 | type: 'string', 16 | label: 'Client ID', 17 | value: '', 18 | required: true, 19 | placeholder: 'your-things-client-id', 20 | oauthQueryParameter: '' 21 | }, 22 | { 23 | name: 'clientSecret', 24 | description: 'The client secret provided when registering the LPWan', 25 | help: '', 26 | type: 'string', 27 | label: 'Client Secret', 28 | value: '', 29 | required: true, 30 | placeholder: 'e.g. ZDTXlylatAHYPDBOXx...', 31 | oauthQueryParameter: '' 32 | }, 33 | { 34 | name: 'username', 35 | description: 'The username of the TTN admin account', 36 | help: '', 37 | type: 'string', 38 | label: 'Username', 39 | value: '', 40 | required: false, 41 | placeholder: 'myTTNUsername', 42 | oauthQueryParameter: '' 43 | }, 44 | { 45 | name: 'password', 46 | description: 'The password of the TTN admin account', 47 | help: '', 48 | type: 'password', 49 | label: 'Password', 50 | value: '', 51 | required: false, 52 | placeholder: 'myTTNPassword', 53 | oauthQueryParameter: '' 54 | } 55 | ], 56 | oauthRequestUrlQueryParams: [ 57 | { 58 | name: 'response_type', 59 | valueSource: 'value', 60 | value: 'code' 61 | }, 62 | { 63 | name: 'client_id', 64 | valueSource: 'protocolHandlerNetworkField', 65 | protocolHandlerNetworkField: 'clientId' 66 | }, 67 | { 68 | name: 'redirect_uri', 69 | valueSource: 'frontEndOauthReturnUri' 70 | } 71 | ], 72 | oauthResponseUrlQueryParams: [ 'code' ], 73 | oauthResponseUrlErrorParams: [ 'error', 'error_description' ] 74 | } 75 | -------------------------------------------------------------------------------- /app/networkProtocols/register.js: -------------------------------------------------------------------------------- 1 | function registerLora1 (networkProtocolModel, loraNwkType) { 2 | return networkProtocolModel.upsert({ 3 | name: 'ChirpStack', 4 | networkTypeId: loraNwkType.id, 5 | protocolHandler: 'LoraOpenSource/v1', 6 | networkProtocolVersion: '1.0' 7 | }) 8 | } 9 | 10 | async function registerLora2 (networkProtocolModel, loraNwkType) { 11 | let me = { 12 | name: 'ChirpStack', 13 | networkTypeId: loraNwkType.id, 14 | protocolHandler: 'LoraOpenSource/v2', 15 | networkProtocolVersion: '2.0' 16 | } 17 | try { 18 | const [records] = await networkProtocolModel.list({ search: me.name, networkProtocolVersion: '1.0' }) 19 | if (records.length) { 20 | me.masterProtocolId = records[0].id 21 | } 22 | } 23 | catch (err) { 24 | // ignore error 25 | } 26 | await networkProtocolModel.upsert(me) 27 | } 28 | 29 | function registerLoriot (networkProtocolModel, loraNwkType) { 30 | return networkProtocolModel.upsert({ 31 | name: 'Loriot', 32 | networkTypeId: loraNwkType.id, 33 | protocolHandler: 'Loriot/v4', 34 | networkProtocolVersion: '4.0' 35 | }) 36 | } 37 | 38 | function registerTtnV2 (networkProtocolModel, loraNwkType) { 39 | return networkProtocolModel.upsert({ 40 | name: 'The Things Network', 41 | networkTypeId: loraNwkType.id, 42 | protocolHandler: 'TheThingsNetwork/v2', 43 | networkProtocolVersion: '2.0' 44 | }) 45 | } 46 | 47 | function registerIP (networkProtocolModel, ipNwkType) { 48 | return networkProtocolModel.upsert({ 49 | name: 'IP', 50 | networkTypeId: ipNwkType.id, 51 | protocolHandler: 'IP' 52 | }) 53 | } 54 | 55 | module.exports = async function registerNetworkProtocols (modelAPI) { 56 | const [ loraNwkType, ipNwkType ] = await Promise.all([ 57 | modelAPI.networkTypes.loadByName('LoRa'), 58 | modelAPI.networkTypes.loadByName('IP') 59 | ]) 60 | await registerLora1(modelAPI.networkProtocols, loraNwkType) 61 | await registerLora2(modelAPI.networkProtocols, loraNwkType) 62 | await registerLoriot(modelAPI.networkProtocols, loraNwkType) 63 | await registerTtnV2(modelAPI.networkProtocols, loraNwkType) 64 | await registerIP(modelAPI.networkProtocols, ipNwkType) 65 | } 66 | -------------------------------------------------------------------------------- /app/reportingProtocols/postHandler.js: -------------------------------------------------------------------------------- 1 | // General libraries in use in this module. 2 | var { logger } = require('../log') 3 | 4 | var request = require('request') 5 | 6 | //* ***************************************************************************** 7 | // POSTs the data object as JSON 8 | //* ***************************************************************************** 9 | // Supports the canned API format of reportingProtocols. Method name is 10 | // "report", taking the parameters: 11 | // 12 | // dataObject - The JSON data object to report 13 | // url - The URL to report to 14 | // appName - The application name we are reporting to 15 | // 16 | // Returns a Promise that sends the report. 17 | module.exports = class PostReportingProtocol { 18 | report (dataObject, url, appName) { 19 | return new Promise(function (resolve, reject) { 20 | // Set up the request options. 21 | var options = {} 22 | options.method = 'POST' 23 | options.uri = url 24 | options.headers = {} 25 | options.headers[ 'Content-Type' ] = 'application/json' 26 | options.headers.appid = appName 27 | if (dataObject === null) dataObject = {} 28 | options.json = dataObject 29 | request(options, function (error, response, body) { 30 | if (error) { 31 | logger.error('Error reporting data (' + 32 | JSON.stringify(dataObject) + 33 | ') for ' + appName + 34 | ' to ' + url + 35 | ':', error) 36 | reject(error) 37 | } 38 | else { 39 | resolve(response) 40 | } 41 | }) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/rest-server.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const https = require('https') 3 | const fs = require('fs') 4 | 5 | function createRestServer (app) { 6 | const opts = { 7 | key: fs.readFileSync(config.ssl_key_file), 8 | cert: fs.readFileSync(config.ssl_cert_file), 9 | requestCert: true, 10 | rejectUnauthorized: false 11 | } 12 | if (config.ssl_ca_file) { 13 | opts.ca = fs.readFileSync(config.ssl_ca_file) 14 | } 15 | if (config.ssl_crl_file) { 16 | opts.crl = fs.readFileSync(config.ssl_crl_file) 17 | } 18 | return https.createServer(opts, app) 19 | } 20 | 21 | module.exports = { 22 | createRestServer 23 | } 24 | -------------------------------------------------------------------------------- /app/rest/restSessions.js: -------------------------------------------------------------------------------- 1 | // General libraries in use in this module. 2 | var { logger } = require('../log') 3 | 4 | var restServer 5 | var modelAPI 6 | 7 | exports.initialize = function (app, server) { 8 | restServer = server 9 | modelAPI = server.modelAPI 10 | 11 | /********************************************************************* 12 | * Sessions API 13 | ********************************************************************/ 14 | /** 15 | * Creates a new session on behalf of the user. 16 | * 17 | * @api {post} /api/sessions Create Session 18 | * @apiGroup Sessions 19 | * @apiDescription Returns a JWT token in the response body that the 20 | * caller is to put into the Authorize header, prepended with "Bearer ", 21 | * for any authorized access to other REST interfaces. 22 | * @apiParam {string} login_username The user's username 23 | * @apiParam {string} login_password The user's password 24 | * @apiExample {json} Example body: 25 | * { 26 | * "login_username": "admin", 27 | * "login_password": "secretshhh" 28 | * } 29 | * @apiSuccess (200) {string} token The JWT token for the user. 30 | * @apiVersion 1.2.1 31 | */ 32 | app.post('/api/sessions', function (req, res, next) { 33 | modelAPI.sessions.authorize(req, res, next).then( 34 | token => { 35 | restServer.respond(res, null, token) 36 | }, 37 | err => { 38 | console.log(err) 39 | restServer.respond(res, err) 40 | } 41 | ) 42 | }) 43 | 44 | /** 45 | * Ends the session. 46 | * 47 | * @api {delete} /api/sessions Delete Session 48 | * @apiGroup Sessions 49 | * @apiDescription Deletes the session. 50 | * @apiVersion 1.2.1 51 | */ 52 | app.delete('/api/sessions', [ restServer.isLoggedIn ], function (req, res, next) { 53 | modelAPI.sessions.delete(req, res, next).then(function () { 54 | restServer.respond(res, 204) 55 | }) 56 | .catch(function (err) { 57 | logger.error('Error on session logout: ', err) 58 | restServer.respond(res, err) 59 | }) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /bin/build-ui: -------------------------------------------------------------------------------- 1 | cd ../lpwanserver-web-client 2 | npm run build 3 | cd ../lpwanserver 4 | rm -rf app/generated/public 5 | mkdir -p app/generated/public 6 | mv ../lpwanserver-web-client/build/* app/generated/public 7 | -------------------------------------------------------------------------------- /bin/clean.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { promisify } = require('util') 4 | const exec = promisify(require('child_process').exec) 5 | const { imageTags } = require('./lib/package') 6 | 7 | async function main () { 8 | await removeContainersAndVolumes() 9 | await removeDockerImages() 10 | } 11 | 12 | main().catch(e => console.error(e)) 13 | 14 | async function removeContainersAndVolumes () { 15 | const containers = [ 16 | 'lpwanserver_dev_unit_test', 17 | 'lpwanserver_dev_api_test', 18 | 'lpwanserver_dev_e2e_test', 19 | 'lpwanserver_dev_prisma', 20 | 'lpwanserver_dev_postgres', 21 | 'lpwanserver_dev_redis', 22 | 'lpwanserver_dev_chirp_postgres', 23 | 'lpwanserver_dev_chirp_redis', 24 | 'lpwanserver_dev_chirpnwksvr', 25 | 'lpwanserver_dev_chirpnwksvr1', 26 | 'lpwanserver_dev_chirpappsvr', 27 | 'lpwanserver_dev_chirpappsvr1', 28 | 'lpwanserver_dev_chirp_mosquitto' 29 | ] 30 | 31 | const volumes = [ 32 | 'lpwanserver_dev_postgres', 33 | 'lpwanserver_dev_redis', 34 | 'lpwanserver_dev_chirp_postgresqldata', 35 | 'lpwanserver_dev_chirp_redisdata', 36 | 'chirpstack_lpwanserver_dev_chirp_postgresqldata', 37 | 'chirpstack_lpwanserver_dev_chirp_redisdata' 38 | ] 39 | 40 | const logErr = () => { 41 | // console.log(e.message) 42 | } 43 | 44 | await Promise.all([ 45 | exec(`docker container rm ${containers.join(' ')} --force`).catch(logErr), 46 | exec(`docker volume rm ${volumes.join(' ')} --force`).catch(logErr) 47 | ]) 48 | } 49 | 50 | function removeDockerImages () { 51 | const tags = Object.keys(imageTags).reduce((acc, x) => { 52 | acc.push(imageTags[x]) 53 | return acc 54 | }, []) 55 | return exec(`docker rmi ${tags.join(' ')} --force`) 56 | } 57 | -------------------------------------------------------------------------------- /bin/demo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$1" = "stop" ] 4 | then 5 | docker-compose -f docker-compose.yml down 6 | docker-compose -f development/chirpstack/docker-compose.yml down 7 | docker-compose -f development/databases/docker-compose.yml down 8 | exit 0 9 | fi 10 | 11 | # Remove old containers, volumes, and images 12 | ./bin/clean.js 13 | 14 | # Start ChirpStacks 15 | docker-compose -f development/chirpstack/docker-compose.yml up -d 16 | 17 | # Start LPWAN Server databases and prisma 18 | docker-compose -f development/databases/docker-compose.yml up -d 19 | 20 | # Build UI 21 | ./bin/build-ui 22 | 23 | # Deploy Prisma 24 | ./development/bin/manage-db deploy 25 | 26 | # Seed Demo Data 27 | ./bin/demo-seed.js 28 | 29 | # Start demo 30 | docker-compose -f docker-compose.yml up -d --build 31 | -------------------------------------------------------------------------------- /bin/demo-seed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Lora1 = require('../test/networks/lora-v1') 4 | const Lora2 = require('../test/networks/lora-v2') 5 | 6 | async function main () { 7 | Lora1.network.baseUrl = Lora1.network.baseUrl.replace('chirpstack_app_svr_1:8080', 'localhost:8081') 8 | Lora2.network.baseUrl = Lora2.network.baseUrl.replace('chirpstack_app_svr:8080', 'localhost:8082') 9 | await Promise.all([ 10 | Lora1.setup(), 11 | Lora2.setup() 12 | ]) 13 | } 14 | 15 | main().catch(err => { 16 | if (err) console.error(err.toString()) 17 | process.exit(1) 18 | }) 19 | -------------------------------------------------------------------------------- /bin/lib/package.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { execSync } = require('child_process') 3 | const component = require('../../component.json') 4 | 5 | const { registry, name, version } = component 6 | let { RC_TAG } = process.env 7 | 8 | if (!RC_TAG) { 9 | const buildNumber = process.env.TRAVIS_BUILD_NUMBER || component.build 10 | RC_TAG = `${version}-${buildNumber}-rc` 11 | } 12 | 13 | const imageTags = { 14 | releaseCandidate: `${registry}/${name}:${RC_TAG}`, 15 | latest: `${registry}/${name}:latest`, 16 | version: `${registry}/${name}:${version}` 17 | } 18 | 19 | const ROOT = path.join(__dirname, '../..') 20 | const opts = { cwd: ROOT, stdio: 'inherit' } 21 | 22 | function packageRestServer () { 23 | execSync(`docker build -f Dockerfile -t ${imageTags.releaseCandidate} -t ${imageTags.latest} .`, opts) 24 | } 25 | 26 | module.exports = { 27 | imageTags, 28 | packageRestServer 29 | } 30 | -------------------------------------------------------------------------------- /bin/package.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./lib/package').packageRestServer() 4 | -------------------------------------------------------------------------------- /bin/release.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { execSync } = require('child_process') 3 | const { imageTags } = require('./lib/package') 4 | const component = require('../component.json') 5 | const path = require('path') 6 | 7 | const ROOT = path.join(__dirname, '..') 8 | const opts = { cwd: ROOT, stdio: 'inherit' } 9 | 10 | const gitTag = `v${component.version}` 11 | 12 | // Push a version tag for docker 13 | execSync(`docker pull ${imageTags.releaseCandidate}`, opts) 14 | execSync(`docker tag ${imageTags.releaseCandidate} ${imageTags.version}`, opts) 15 | execSync(`docker push ${imageTags.version}`, opts) 16 | 17 | // Add a tag in git 18 | execSync(`git tag -a ${gitTag} -m "Version ${component.version}"`, opts) 19 | execSync(`git push origin ${gitTag}`, opts) -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lpwanserver", 3 | "registry": "lpwanserver", 4 | "version": "1.2.2", 5 | "build": "1" 6 | } 7 | -------------------------------------------------------------------------------- /development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15 2 | 3 | WORKDIR /usr/src 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | EXPOSE 3200 10 | 11 | CMD ["npm", "run", "dev"] 12 | -------------------------------------------------------------------------------- /development/bin/generate-development-certificates: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | OUT=certs 4 | CONF=development/cert-conf 5 | 6 | mkdir -p $OUT 7 | 8 | # Certificate Authority 9 | openssl req -new -x509 -days 9999 -config "$CONF/ca.cnf" -keyout "$OUT/ca-key.pem" -out "$OUT/ca-crt.pem" 10 | 11 | # Server 12 | openssl genrsa -out "$OUT/server-key.pem" 4096 13 | 14 | openssl req -new -config "$CONF/server.cnf" -key "$OUT/server-key.pem" -out "$OUT/server-csr.pem" 15 | 16 | openssl x509 -req -extfile "$CONF/server.cnf" -days 999 -passin "pass:password" -in "$OUT/server-csr.pem" \ 17 | -CA "$OUT/ca-crt.pem" -CAkey "$OUT/ca-key.pem" -CAcreateserial -out "$OUT/server-crt.pem" -extfile "$CONF/server.ext" 18 | 19 | # IP Device clients 20 | openssl genrsa -out "$OUT/client-catm1-key.pem" 4096 21 | 22 | openssl genrsa -out "$OUT/client-nbiot-key.pem" 4096 23 | 24 | openssl req -new -config "$CONF/client-catm1.cnf" -key "$OUT/client-catm1-key.pem" -out "$OUT/client-catm1-csr.pem" 25 | 26 | openssl req -new -config "$CONF/client-nbiot.cnf" -key "$OUT/client-nbiot-key.pem" -out "$OUT/client-nbiot-csr.pem" 27 | 28 | openssl x509 -req -extfile "$CONF/client-catm1.cnf" -days 999 -passin "pass:password" \ 29 | -in "$OUT/client-catm1-csr.pem" -CA "$OUT/ca-crt.pem" -CAkey "$OUT/ca-key.pem" \ 30 | -CAcreateserial -out "$OUT/client-catm1-crt.pem" -extfile "$CONF/client.ext" 31 | 32 | openssl x509 -req -extfile "$CONF/client-nbiot.cnf" -days 999 -passin "pass:password" \ 33 | -in "$OUT/client-nbiot-csr.pem" -CA "$OUT/ca-crt.pem" -CAkey "$OUT/ca-key.pem" \ 34 | -CAcreateserial -out "$OUT/client-nbiot-crt.pem" -extfile "$CONF/client.ext" 35 | -------------------------------------------------------------------------------- /development/bin/manage-db: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$1" = "start" ] 4 | then 5 | docker-compose -f development/databases/docker-compose.yml up -d 6 | exit 0 7 | fi 8 | 9 | if [ "$1" = "deploy" ] 10 | then 11 | sleep 5s 12 | prisma_url=http://localhost:4466/lpwanserver/dev npm run prisma -- deploy 13 | sleep 2s 14 | exit 0 15 | fi 16 | 17 | if [ "$1" = "reset" ] 18 | then 19 | prisma_url='http://localhost:4466/lpwanserver/dev' npm run prisma -- seed --reset 20 | exit 0 21 | fi 22 | 23 | if [ "$1" = "stop" ] 24 | then 25 | docker-compose -f development/databases/docker-compose.yml down 26 | exit 0 27 | fi 28 | -------------------------------------------------------------------------------- /development/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Start databases and prisma 4 | ./development/bin/manage-db start 5 | ./development/bin/manage-db deploy 6 | 7 | # Start Lora Servers 8 | docker-compose -f development/chirpstack/docker-compose.yml up -d 9 | 10 | # Start development docker container 11 | docker-compose -f development/docker-compose.yml up 12 | 13 | # Stop Lora Servers 14 | docker-compose -f development/chirpstack/docker-compose.yml down 15 | 16 | # Stop databases and prisma 17 | ./development/bin/manage-db stop 18 | -------------------------------------------------------------------------------- /development/cert-conf/ca.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = CA_default 3 | 4 | [ CA_default ] 5 | serial = ca-serial 6 | crl = ca-crl.pem 7 | database = ca-database.txt 8 | name_opt = CA_default 9 | cert_opt = CA_default 10 | default_crl_days = 9999 11 | default_md = md5 12 | 13 | [ req ] 14 | default_bits = 4096 15 | days = 9999 16 | distinguished_name = req_distinguished_name 17 | attributes = req_attributes 18 | prompt = no 19 | output_password = password 20 | 21 | [ req_distinguished_name ] 22 | C = US 23 | ST = CO 24 | L = Louisville 25 | O = CableLabs 26 | OU = lpwanserver 27 | CN = ca 28 | emailAddress = certs@example.com 29 | 30 | [ req_attributes ] 31 | challengePassword = test 32 | -------------------------------------------------------------------------------- /development/cert-conf/client-catm1.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 4096 3 | days = 9999 4 | distinguished_name = req_distinguished_name 5 | attributes = req_attributes 6 | prompt = no 7 | x509_extensions = v3_ca 8 | 9 | [ req_distinguished_name ] 10 | C = US 11 | ST = CO 12 | L = Louisville 13 | O = CableLabs 14 | OU = lpwanserver 15 | CN = 00:11:22:33:44:55:66:77 16 | emailAddress = certs@example.com 17 | 18 | [ req_attributes ] 19 | challengePassword = password 20 | 21 | [ v3_ca ] 22 | authorityInfoAccess = @issuer_info 23 | 24 | [ issuer_info ] 25 | OCSP;URI.0 = http://ocsp.example.com/ 26 | caIssuers;URI.0 = http://example.com/ca.cert 27 | -------------------------------------------------------------------------------- /development/cert-conf/client-nbiot.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 4096 3 | days = 9999 4 | distinguished_name = req_distinguished_name 5 | attributes = req_attributes 6 | prompt = no 7 | x509_extensions = v3_ca 8 | 9 | [ req_distinguished_name ] 10 | C = US 11 | ST = CO 12 | L = Louisville 13 | O = CableLabs 14 | OU = lpwanserver 15 | CN = 11:22:33:44:55:66:77:88 16 | emailAddress = certs@example.com 17 | 18 | [ req_attributes ] 19 | challengePassword = password 20 | 21 | [ v3_ca ] 22 | authorityInfoAccess = @issuer_info 23 | 24 | [ issuer_info ] 25 | OCSP;URI.0 = http://ocsp.example.com/ 26 | caIssuers;URI.0 = http://example.com/ca.cert 27 | -------------------------------------------------------------------------------- /development/cert-conf/client.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | DNS.1 = localhost 8 | DNS.2 = ip-device 9 | -------------------------------------------------------------------------------- /development/cert-conf/server.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 4096 3 | days = 9999 4 | distinguished_name = req_distinguished_name 5 | attributes = req_attributes 6 | prompt = no 7 | x509_extensions = v3_ca 8 | 9 | [ req_distinguished_name ] 10 | C = US 11 | ST = CO 12 | L = Louisville 13 | O = CableLabs 14 | OU = lpwanserver 15 | CN = localhost 16 | emailAddress = certs@example.com 17 | 18 | [ req_attributes ] 19 | challengePassword = password 20 | 21 | [ v3_ca ] 22 | authorityInfoAccess = @issuer_info 23 | 24 | [ issuer_info ] 25 | OCSP;URI.0 = http://ocsp.example.com/ 26 | caIssuers;URI.0 = http://example.com/ca.cert 27 | -------------------------------------------------------------------------------- /development/cert-conf/server.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | DNS.1 = localhost 8 | DNS.2 = lpwanserver 9 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/lora-app-server/certs/http.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEpDCCAowCCQD85f2p9nWwaTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls 3 | b2NhbGhvc3QwHhcNMTgwNDAzMDk1OTU4WhcNMTkwNDAzMDk1OTU4WjAUMRIwEAYD 4 | VQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCm 5 | ylm1uvBfVYB3PnHzpSJm02Vi2dvZtpBPnrGXS8ZcCNiwHs0I/Z2VGJMtTWDUAUOL 6 | o1vt46F4JscvKFF5+hXs3C/3BVgwfL5He7NdI3JOdq6Pla40tsuBSg8y9ZC13e2L 7 | jst81eeW3R6dYYob3H8h5oEQjqwoWLyPiN7ZlpYKoROawaXVxeAr/MhZyWEdZtsE 8 | 5h99hiRX8pIE4fp6gr8UtDTklx3a3lk5S6jBesvV6K/Myc8yjgXrPKnNXwbturkL 9 | dQJRmAfRTHu9ylSMXeKR9Ygj5ie3dzNAKZ3Y4BDTJ6RlmQTpR1pZSNluoK1Dhm0q 10 | tNOGvaMZe20OOUvROFIzY6LhbpYTLxgUoCCa9ZVhllCLHl55EOswubFoYIGUEfGH 11 | Mno9g6MSmInsQha/vcPzYn08E1/r8t4dBb1lXarcpLyPEsdOCUCgjGlEeHP8VGeb 12 | WV9L95zblIty4G10/dSMlxQyNHj0bSWdKzC1nq6vZ5Di3Vtr+DUDlr7YjLVqP+rQ 13 | 026Yamc/8B7Hh8/uXLQ33wSyTyElHkNPf+zqjvocEAcN0itrJwTe2r5c6j+R8bPj 14 | LYu/oot+s/A76u5bqr/VDC3USNM4knYvhJxt4a7QNdy7SA3X3W2T7nCEF+wOGqPi 15 | QDp/SZlSPtf031d60UOEYGe1Mh8vs2+9jD2bPGx/twIDAQABMA0GCSqGSIb3DQEB 16 | CwUAA4ICAQCPWR/q0vMM7SjV3k/2ZjzLgGRCZiDLrSxALa/6nKEda+v8OBWUoPkH 17 | fzrm4XNi+THjZputAANtLpeY5eDvO2R2X3+p6q/+W0SgFfQCsymG9T2uYxnaD2w5 18 | 1hJo0bj4BN5Hw3aqSHulJE82z1NAvQWZAf+O6J3HowJX0u2SQwEbSGLLim6sf1Pv 19 | 7ZX3o3u3lDY+BjHtzzFZUprWXugAoyfeRPb4tvUL6s5pUhcMy7Dn5ly5SHXrUwRL 20 | zfoMnYZLcNgd9mQSsj9Qm0DCVekl9AcGl42LNgJUfF+8d12TglZnDX+6sgHI77KI 21 | gXEjjSj7N5i1M6jqkdFjQBKRsMc0bQj4hB4Gx4qeUtv+YXq8tvJut6Lt8KL9KBb4 22 | L6DTWByBTXuBpCXiNoAn5sXzstLfTq1PcWNnmxEZyib6hmT0NnAXlVJtcNwW3P76 23 | yhWCFWWQCb8LBXgo8RlNEHWqnCNo3cHoQDtQ5AOQZRBh3YghqvCaKq3vjz6phT8D 24 | ErzF0sEmN88EPfN0gV8IJqNxeLVf3Wjy/vWF5cWqeV8DuTshnxhwYTgOWD/6rGd4 25 | UOjWsMHDY9/Sv4+aLu8JsYug6BID8uCGLjqxlsTKq+nwaXGEYDfxeY4cDYoyExUn 26 | NuPbqGyr4eyFkpgViRPIGjBJXHWs1ejEJcpNSYRPUxbaBpArG/UOjw== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/lora-app-server/lora-app-server.toml: -------------------------------------------------------------------------------- 1 | # See https://www.loraserver.io/lora-app-server/install/config/ for a full 2 | # configuration example and documentation. 3 | 4 | [postgresql] 5 | dsn="postgres://loraserver_as:loraserver_as@chirp_postgresql/loraserver_as?sslmode=disable" 6 | 7 | [redis] 8 | url="redis://chirp_redis:6379/1" 9 | 10 | [application_server.integration.mqtt] 11 | server="tcp://chirp_mosquitto:1883" 12 | username="" 13 | password="" 14 | 15 | [application_server.api] 16 | public_host="appserver:8001" 17 | 18 | 19 | [application_server.external_api] 20 | bind="0.0.0.0:8080" 21 | tls_cert="/etc/lora-app-server/certs/http.pem" 22 | tls_key="/etc/lora-app-server/certs/http-key.pem" 23 | jwt_secret="verysecret" 24 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/lora-nwk-server/loraserver.toml: -------------------------------------------------------------------------------- 1 | # See https://www.loraserver.io/loraserver/install/config/ for a full 2 | # configuration example and documentation. 3 | 4 | [postgresql] 5 | dsn="postgres://loraserver_ns:loraserver_ns@chirp_postgresql/loraserver_ns?sslmode=disable" 6 | 7 | [redis] 8 | url="redis://chirp_redis:6379/1" 9 | 10 | [network_server] 11 | net_id="000000" 12 | 13 | [network_server.band] 14 | # LoRaWAN band to use. 15 | # 16 | # Valid values are: 17 | # * AS_923 18 | # * AU_915_928 19 | # * CN_470_510 20 | # * CN_779_787 21 | # * EU_433 22 | # * EU_863_870 23 | # * IN_865_867 24 | # * KR_920_923 25 | # * RU_864_870 26 | # * US_902_928 27 | name="US_902_928" 28 | 29 | # Enable only a given sub-set of channels 30 | # 31 | # Use this when ony a sub-set of the by default enabled channels are being 32 | # used. For example when only using the first 8 channels of the US band. 33 | # 34 | # Example: 35 | # enabled_uplink_channels=[0, 1, 2, 3, 4, 5, 6, 7] 36 | enabled_uplink_channels=[] 37 | 38 | # Extra channel configuration. 39 | # 40 | # Use this for LoRaWAN regions where it is possible to extend the by default 41 | # available channels with additional channels (e.g. the EU band). 42 | # The first 5 channels will be configured as part of the OTAA join-response 43 | # (using the CFList field). 44 | # The other channels (or channel / data-rate changes) will be (re)configured 45 | # using the NewChannelReq mac-command. 46 | # 47 | # Example: 48 | # [[network_server.network_settings.extra_channels]] 49 | # frequency=867100000 50 | # min_dr=0 51 | # max_dr=5 52 | 53 | # [[network_server.network_settings.extra_channels]] 54 | # frequency=867300000 55 | # min_dr=0 56 | # max_dr=5 57 | 58 | # [[network_server.network_settings.extra_channels]] 59 | # frequency=867500000 60 | # min_dr=0 61 | # max_dr=5 62 | 63 | # [[network_server.network_settings.extra_channels]] 64 | # frequency=867700000 65 | # min_dr=0 66 | # max_dr=5 67 | 68 | # [[network_server.network_settings.extra_channels]] 69 | # frequency=867900000 70 | # min_dr=0 71 | # max_dr=5 72 | 73 | [network_server.gateway.backend.mqtt] 74 | server="tcp://chirp_mosquitto:1883" 75 | username="" 76 | password="" 77 | 78 | [join_server.default] 79 | server="http://appserver:8003" 80 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/mosquitto/mosquitto.conf: -------------------------------------------------------------------------------- 1 | # This is a Mosquitto configuration file that creates a listener on port 1883 2 | # that allows unauthenticated access. 3 | 4 | listener 1883 5 | allow_anonymous true 6 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/postgresql/initdb/001-init-chirpstack_ns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 5 | create role chirpstack_ns with login password 'chirpstack_ns'; 6 | create database chirpstack_ns with owner chirpstack_ns; 7 | EOSQL 8 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/postgresql/initdb/002-init-chirpstack_as.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 5 | create role chirpstack_as with login password 'chirpstack_as'; 6 | create database chirpstack_as with owner chirpstack_as; 7 | EOSQL 8 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/postgresql/initdb/003-chirpstack_as_trgm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname="chirpstack_as" <<-EOSQL 5 | create extension pg_trgm; 6 | EOSQL 7 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/postgresql/initdb/004-chirpstack_as_hstore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname="chirpstack_as" <<-EOSQL 5 | create extension hstore; 6 | EOSQL 7 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/postgresql/initdb/005-init-loraserver_ns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 5 | create role loraserver_ns with login password 'loraserver_ns'; 6 | create database loraserver_ns with owner loraserver_ns; 7 | EOSQL 8 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/postgresql/initdb/006-init-loraserver_as.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 5 | create role loraserver_as with login password 'loraserver_as'; 6 | create database loraserver_as with owner loraserver_as; 7 | EOSQL 8 | -------------------------------------------------------------------------------- /development/chirpstack/configuration/postgresql/initdb/007-loraserver_as_trgm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname="loraserver_as" <<-EOSQL 5 | create extension pg_trgm; 6 | EOSQL 7 | -------------------------------------------------------------------------------- /development/chirpstack/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | chirpstack_nwk_svr: 5 | image: chirpstack/chirpstack-network-server:3 6 | container_name: lpwanserver_dev_chirpnwksvr 7 | networks: 8 | - lpwanserver_dev 9 | environment: 10 | - POSTGRESQL.DSN=postgres://chirpstack_ns:chirpstack_ns@chirp_postgresql/chirpstack_ns?sslmode=disable 11 | - REDIS.URL=redis://chirp_redis:6379/2 12 | - NETWORK_SERVER.BAND.NAME=US_902_928 13 | - NETWORK_SERVER.GATEWAY.BACKEND.MQTT.SERVER=tcp://chirp_mosquitto:1883 14 | - JOIN_SERVER.DEFAULT.SERVER=http://chirpstack_app_svr:8003 15 | 16 | chirpstack_app_svr: 17 | image: chirpstack/chirpstack-application-server:3 18 | container_name: lpwanserver_dev_chirpappsvr 19 | networks: 20 | - lpwanserver_dev 21 | ports: 22 | - 8082:8080 23 | environment: 24 | - POSTGRESQL.DSN=postgres://chirpstack_as:chirpstack_as@chirp_postgresql/chirpstack_as?sslmode=disable 25 | - REDIS.URL=redis://chirp_redis:6379/2 26 | - APPLICATION_SERVER.INTEGRATION.MQTT.SERVER=tcp://chirp_mosquitto:1883 27 | - APPLICATION_SERVER.API.PUBLIC_HOST=chirpstack_app_svr:8001 28 | - APPLICATION_SERVER.EXTERNAL_API.JWT_SECRET=verysecret 29 | 30 | chirpstack_nwk_svr_1: 31 | image: loraserver/loraserver:1 32 | container_name: lpwanserver_dev_chirpnwksvr1 33 | networks: 34 | - lpwanserver_dev 35 | volumes: 36 | - ./configuration/lora-nwk-server:/etc/loraserver 37 | 38 | chirpstack_app_svr_1: 39 | image: loraserver/lora-app-server:1 40 | container_name: lpwanserver_dev_chirpappsvr1 41 | networks: 42 | - lpwanserver_dev 43 | ports: 44 | - 8081:8080 45 | volumes: 46 | - ./configuration/lora-app-server:/etc/lora-app-server 47 | 48 | chirp_postgresql: 49 | image: postgres:10.3-alpine 50 | container_name: lpwanserver_dev_chirp_postgres 51 | networks: 52 | - lpwanserver_dev 53 | volumes: 54 | - ./configuration/postgresql/initdb:/docker-entrypoint-initdb.d 55 | - lpwanserver_dev_chirp_postgresqldata:/var/lib/postgresql/data 56 | 57 | chirp_redis: 58 | image: redis:5-alpine 59 | container_name: lpwanserver_dev_chirp_redis 60 | volumes: 61 | - lpwanserver_dev_chirp_redisdata:/data 62 | networks: 63 | - lpwanserver_dev 64 | ports: 65 | - 6380:6379 66 | 67 | chirp_mosquitto: 68 | image: eclipse-mosquitto 69 | networks: 70 | - lpwanserver_dev 71 | ports: 72 | - 1884:1883 73 | volumes: 74 | - ./configuration/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf 75 | 76 | volumes: 77 | lpwanserver_dev_chirp_postgresqldata: 78 | lpwanserver_dev_chirp_redisdata: 79 | 80 | networks: 81 | lpwanserver_dev: 82 | name: lpwanserver_dev 83 | -------------------------------------------------------------------------------- /development/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log_level": "debug", 3 | "ssl_key_file": "../certs/server-key.pem", 4 | "ssl_cert_file": "../certs/server-crt.pem", 5 | "ssl_ca_file": "../certs/ca-crt.pem", 6 | "jwt_secret": "replace-this-value-for-your-installation", 7 | "port": 3200, 8 | "base_url": "https://lpwanserver:3200", 9 | "prisma_url": "http://prisma:4466/lpwanserver/dev", 10 | "redis_url": "redis://redis:6379/1" 11 | } 12 | -------------------------------------------------------------------------------- /development/data/json/lora1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "application": { 4 | "description": "string", 5 | "id": "string", 6 | "name": "string", 7 | "organizationID": "string", 8 | "payloadCodec": "string", 9 | "payloadDecoderScript": "string", 10 | "payloadEncoderScript": "string", 11 | "serviceProfileID": "string" 12 | } 13 | }, 14 | { 15 | "deviceProfile": { 16 | "createdAt": "string", 17 | "deviceProfile": { 18 | "classBTimeout": 0, 19 | "classCTimeout": 0, 20 | "deviceProfileID": "string", 21 | "factoryPresetFreqs": [ 22 | 0 23 | ], 24 | "macVersion": "string", 25 | "maxDutyCycle": 0, 26 | "maxEIRP": 0, 27 | "pingSlotDR": 0, 28 | "pingSlotFreq": 0, 29 | "pingSlotPeriod": 0, 30 | "regParamsRevision": "string", 31 | "rfRegion": "string", 32 | "rxDROffset1": 0, 33 | "rxDataRate2": 0, 34 | "rxDelay1": 0, 35 | "rxFreq2": 0, 36 | "supports32bitFCnt": true, 37 | "supportsClassB": true, 38 | "supportsClassC": true, 39 | "supportsJoin": true 40 | }, 41 | "name": "string", 42 | "networkServerID": "string", 43 | "organizationID": "string", 44 | "updatedAt": "string" 45 | } 46 | }, 47 | { 48 | "device": { 49 | "applicationID": "string", 50 | "description": "string", 51 | "devEUI": "string", 52 | "deviceProfileID": "string", 53 | "deviceStatusBattery": 0, 54 | "deviceStatusMargin": 0, 55 | "lastSeenAt": "string", 56 | "name": "string", 57 | "skipFCntCheck": true 58 | } 59 | }, 60 | { 61 | "integrations": { 62 | "ackNotificationURL": "string", 63 | "dataUpURL": "string", 64 | "errorNotificationURL": "string", 65 | "headers": [ 66 | { 67 | "key": "string", 68 | "value": "string" 69 | } 70 | ], 71 | "id": "string", 72 | "joinNotificationURL": "string" 73 | } 74 | } 75 | ] -------------------------------------------------------------------------------- /development/data/json/lora2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "application": { 4 | "application": { 5 | "description": "string", 6 | "id": "string", 7 | "name": "string", 8 | "organizationID": "string", 9 | "payloadCodec": "string", 10 | "payloadDecoderScript": "string", 11 | "payloadEncoderScript": "string", 12 | "serviceProfileID": "string" 13 | } 14 | } 15 | }, 16 | { 17 | "deviceProfile": { 18 | "createdAt": "2018-09-05T05:28:09.681Z", 19 | "deviceProfile": { 20 | "classBTimeout": 0, 21 | "classCTimeout": 0, 22 | "factoryPresetFreqs": [ 23 | 0 24 | ], 25 | "id": "string", 26 | "macVersion": "string", 27 | "maxDutyCycle": 0, 28 | "maxEIRP": 0, 29 | "name": "string", 30 | "networkServerID": "string", 31 | "organizationID": "string", 32 | "pingSlotDR": 0, 33 | "pingSlotFreq": 0, 34 | "pingSlotPeriod": 0, 35 | "regParamsRevision": "string", 36 | "rfRegion": "string", 37 | "rxDROffset1": 0, 38 | "rxDataRate2": 0, 39 | "rxDelay1": 0, 40 | "rxFreq2": 0, 41 | "supports32BitFCnt": true, 42 | "supportsClassB": true, 43 | "supportsClassC": true, 44 | "supportsJoin": true 45 | }, 46 | "updatedAt": "2018-09-05T05:28:09.682Z" 47 | } 48 | }, 49 | { 50 | "device": { 51 | "device": { 52 | "applicationID": "string", 53 | "description": "string", 54 | "devEUI": "string", 55 | "deviceProfileID": "string", 56 | "name": "string", 57 | "skipFCntCheck": true 58 | }, 59 | "deviceStatusBattery": 0, 60 | "deviceStatusMargin": 0, 61 | "lastSeenAt": "2018-09-05T05:28:09.738Z" 62 | } 63 | }, 64 | { 65 | "integrations": { 66 | "integration": { 67 | "ackNotificationURL": "string", 68 | "applicationID": "string", 69 | "errorNotificationURL": "string", 70 | "headers": [ 71 | { 72 | "key": "string", 73 | "value": "string" 74 | } 75 | ], 76 | "joinNotificationURL": "string", 77 | "uplinkDataURL": "string" 78 | } 79 | } 80 | } 81 | ] -------------------------------------------------------------------------------- /development/data/json/loriot.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "application": { 4 | "_id": 3195929586, 5 | "name": "ApiTest", 6 | "ownerid": 248, 7 | "created": "2018-06-28T16:27:19.980Z", 8 | "tier": 2, 9 | "tierStr": "PoC", 10 | "devices": 1, 11 | "deviceLimit": 10, 12 | "output": "httppush", 13 | "osetup": { 14 | "url": "http://localhost:3200/api/ingest/1/1", 15 | "auth": "" 16 | }, 17 | "overbosity": "full", 18 | "ogwinfo": "rssi", 19 | "orx": true, 20 | "cansend": true, 21 | "canotaa": true, 22 | "suspended": false, 23 | "clientsLimit": 10, 24 | "joinServer": null, 25 | "publishAppSKey": false 26 | } 27 | }, 28 | { 29 | "deviceProfile": { 30 | } 31 | }, 32 | { 33 | "device":{ 34 | "_id": "0080000004001546", 35 | "title": "00-80-00-00-04-00-15-46", 36 | "description": null, 37 | "appeui": "BE7E0000000003F2", 38 | "deveui": "0080000004001546", 39 | "devaddr": "002AF013", 40 | "nwkskey": "93AA920D2CDC0F093109D5884E1E6030", 41 | "appskey": "7D39F3447CAD6D8EBCD32D0DB73E1E4D", 42 | "appkey": "84BC34D6D609B02102596AD77A4F988E", 43 | "seqno": -1, 44 | "seqdn": 0, 45 | "seqq": 0, 46 | "adrCnt": 0, 47 | "subscription": 2, 48 | "txrate": null, 49 | "rxrate": null, 50 | "devclass": "A", 51 | "rxw": 1, 52 | "rx1": { 53 | "delay": 1000000, 54 | "offset": 0 55 | }, 56 | "dutycycle": 0, 57 | "adr": true, 58 | "adrMin": null, 59 | "adrMax": null, 60 | "adrFix": null, 61 | "seqrelax": true, 62 | "seqdnreset": true, 63 | "createdAt": "2018-07-20T18:32:11.557Z", 64 | "bat": null, 65 | "devSnr": null, 66 | "packetLimit": null 67 | } 68 | }, 69 | { 70 | "integrations": { 71 | "output": "httppush", 72 | "osetup": { 73 | "url": "http://localhost:3200/api/ingest/1/1", 74 | "auth": "" 75 | }, 76 | "overbosity": "full", 77 | "ogwinfo": "rssi", 78 | "orx": true 79 | } 80 | } 81 | ] -------------------------------------------------------------------------------- /development/data/json/normalized.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "application": { 4 | "description": "string", 5 | "id": "string", 6 | "name": "string", 7 | "organizationID": "string", 8 | "payloadCodec": "string", 9 | "payloadDecoderScript": "string", 10 | "payloadEncoderScript": "string", 11 | "validationScript": "string", 12 | "serviceProfileID": "string", 13 | "cansend": "boolean", 14 | "deviceLimit": "number", 15 | "devices": "number", 16 | "overbosity": "string", 17 | "ogwinfo": "string", 18 | "orx": "boolean", 19 | "canotaa": "boolean", 20 | "suspended": "boolean", 21 | "clientsLimit": "number", 22 | "joinServer": "object", 23 | "applicationEUI": "string", 24 | "key": "string" 25 | } 26 | }, 27 | { 28 | "deviceProfile": { 29 | "classBTimeout": 0, 30 | "classCTimeout": 0, 31 | "factoryPresetFreqs": [ 32 | 0 33 | ], 34 | "id": "string", 35 | "macVersion": "string", 36 | "maxDutyCycle": 0, 37 | "maxEIRP": 0, 38 | "name": "string", 39 | "networkServerID": "string", 40 | "organizationID": "string", 41 | "pingSlotDR": 0, 42 | "pingSlotFreq": 0, 43 | "pingSlotPeriod": 0, 44 | "regParamsRevision": "string", 45 | "rfRegion": "string", 46 | "rxDROffset1": 0, 47 | "rxDataRate2": 0, 48 | "rxDelay1": 0, 49 | "rxFreq2": 0, 50 | "supports32BitFCnt": true, 51 | "supportsClassB": true, 52 | "supportsClassC": true, 53 | "supportsJoin": true 54 | } 55 | }, 56 | { 57 | "device": { 58 | "applicationID": "string", 59 | "description": "string", 60 | "devEUI": "string", 61 | "deviceProfileID": "string", 62 | "name": "string", 63 | "skipFCntCheck": true, 64 | "deviceStatusBattery": 0, 65 | "deviceStatusMargin": 0, 66 | "lastSeenAt": "2018-09-05T05:28:09.738Z" 67 | } 68 | }, 69 | { 70 | "integrations": { 71 | "ackNotificationURL": "string", 72 | "applicationID": "string", 73 | "errorNotificationURL": "string", 74 | "headers": [ 75 | { 76 | "key": "string", 77 | "value": "string" 78 | } 79 | ], 80 | "joinNotificationURL": "string", 81 | "uplinkDataURL": "string" 82 | } 83 | } 84 | ] -------------------------------------------------------------------------------- /development/data/json/ttn.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "application": [ 4 | { 5 | "app_id": "some-app-id", 6 | "converter": "function Converter(decoded, port) {...", 7 | "decoder": "function Decoder(bytes, port) {...", 8 | "encoder": "Encoder(object, port) {...", 9 | "payload_format": "", 10 | "register_on_join_access_key": "", 11 | "validator": "Validator(converted, port) {..." 12 | }, 13 | { 14 | "id": "cable-labs-prototype", 15 | "name": "Prototype Application for CableLabs Trial", 16 | "euis": [ 17 | "70B3D57ED000FEEA" 18 | ], 19 | "created": "2018-06-20T16:44:16.441Z", 20 | "rights": [ 21 | "settings", 22 | "delete", 23 | "collaborators", 24 | "devices" 25 | ], 26 | "collaborators": [ 27 | { 28 | "username": "dschrimpsherr", 29 | "email": "d.schrimpsher@cablelabs.com", 30 | "rights": [ 31 | "settings", 32 | "delete", 33 | "collaborators", 34 | "devices" 35 | ] 36 | } 37 | ], 38 | "access_keys": [ 39 | { 40 | "name": "default key", 41 | "key": "ttn-account-v2.kMyE-zi7vQXrz1kscOlA0MTNaQB6_D7elsMRG91BAjQ", 42 | "_id": "5b2a84606a41ae0030a913b8", 43 | "rights": [ 44 | "messages:up:r", 45 | "messages:down:w", 46 | "devices" 47 | ] 48 | }, 49 | { 50 | "rights": [ 51 | "settings", 52 | "devices", 53 | "messages:up:r", 54 | "messages:down:w" 55 | ], 56 | "_id": "5b2a8c8c6a41ae0030a9154d", 57 | "key": "ttn-account-v2.HgTv51zRBreL4b3d2eSolzcCdsPZqKLSrjnfEo5KgIs", 58 | "name": "testlora" 59 | } 60 | ] 61 | } 62 | ] 63 | }, 64 | { 65 | "deviceProfile": {} 66 | }, 67 | { 68 | "device": { 69 | "altitude": 0, 70 | "app_id": "some-app-id", 71 | "attributes": { 72 | "key": "", 73 | "value": "" 74 | }, 75 | "description": "Some description of the device", 76 | "dev_id": "some-dev-id", 77 | "latitude": 52.375, 78 | "longitude": 4.887, 79 | "lorawan_device": { 80 | "activation_constraints": "local", 81 | "app_eui": "0102030405060708", 82 | "app_id": "some-app-id", 83 | "app_key": "01020304050607080102030405060708", 84 | "app_s_key": "01020304050607080102030405060708", 85 | "dev_addr": "01020304", 86 | "dev_eui": "0102030405060708", 87 | "dev_id": "some-dev-id", 88 | "disable_f_cnt_check": false, 89 | "f_cnt_down": 0, 90 | "f_cnt_up": 0, 91 | "last_seen": 0, 92 | "nwk_s_key": "01020304050607080102030405060708", 93 | "uses32_bit_f_cnt": true 94 | } 95 | } 96 | }, 97 | { 98 | "integrations": { 99 | } 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /development/databases/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | prisma: 5 | image: prismagraphql/prisma:1.33 6 | container_name: lpwanserver_dev_prisma 7 | networks: 8 | - lpwanserver_dev 9 | ports: 10 | - "4466:4466" 11 | depends_on: 12 | - postgres 13 | environment: 14 | PRISMA_CONFIG: | 15 | port: 4466 16 | databases: 17 | default: 18 | connector: postgres 19 | host: postgres 20 | port: 5432 21 | user: prisma 22 | password: prisma 23 | 24 | postgres: 25 | image: postgres:10.3-alpine 26 | container_name: lpwanserver_dev_postgres 27 | networks: 28 | - lpwanserver_dev 29 | ports: 30 | - 5432 31 | environment: 32 | POSTGRES_USER: prisma 33 | POSTGRES_PASSWORD: prisma 34 | volumes: 35 | - postgres:/var/lib/postgresql/data 36 | 37 | redis: 38 | image: redis:4-alpine 39 | container_name: lpwanserver_dev_redis 40 | networks: 41 | - lpwanserver_dev 42 | ports: 43 | - 6379 44 | volumes: 45 | - redis:/data 46 | 47 | # redis-commander: 48 | # container_name: lpwanserver_dev_redis_commander 49 | # hostname: redis-commander 50 | # image: rediscommander/redis-commander:latest 51 | # environment: 52 | # - REDIS_HOSTS=local:redis:6379 53 | # ports: 54 | # - "8081:8081" 55 | 56 | networks: 57 | lpwanserver_dev: 58 | name: lpwanserver_dev 59 | volumes: 60 | postgres: 61 | name: lpwanserver_dev_postgres 62 | redis: 63 | name: lpwanserver_dev_redis 64 | -------------------------------------------------------------------------------- /development/docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: '3.5' 3 | services: 4 | lpwanserver: 5 | build: 6 | context: ../ 7 | dockerfile: development/Dockerfile 8 | volumes: 9 | - ../app:/usr/src/app 10 | - ../certs:/usr/src/certs 11 | - ./config.json:/usr/src/config.json 12 | ports: 13 | - 3200:3200 14 | networks: 15 | - lpwanserver_dev 16 | environment: 17 | - port=3200 18 | - config_file=../config.json 19 | - public_dir= 20 | networks: 21 | lpwanserver_dev: 22 | name: lpwanserver_dev 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | lpwanserver: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | image: lpwanserver/lpwanserver 9 | container_name: lpwanserver 10 | networks: 11 | - lpwanserver_dev 12 | ports: 13 | - '3200:3200' 14 | environment: 15 | - port=3200 16 | - config_file=../config.json 17 | volumes: 18 | - ./development/config.json:/usr/src/config.json 19 | - ./certs:/usr/src/certs 20 | 21 | networks: 22 | lpwanserver_dev: 23 | name: lpwanserver_dev 24 | -------------------------------------------------------------------------------- /docs/RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releasing Versions 2 | 3 | ### Component.json 4 | 5 | The file `component.json` contains the registry and the name of the application. 6 | It contains the version for which release candidates are pushed to Docker Hub, 7 | as well as the build number, overwritten by a Travis environment variable. 8 | 9 | ### Release candiates 10 | 11 | Release candidates are built in Travis and pushed to Docker Hub. The release candiate 12 | tag name takes the form of VERSION-BUILD_NUMBER-rc. To release a candidate, first 13 | locate the release candidate in docker hub. 14 | 15 | ### Edit Component.json 16 | 17 | Ensure that the version listed in `component.json` is the version you intend to release. 18 | Update it if necessary. 19 | 20 | ### Export the release candidate tag 21 | 22 | For example: 23 | 24 | ``` 25 | $ export RC_TAG=1.0.0-96-rc 26 | ``` 27 | 28 | ### Run the release script 29 | 30 | Run `./bin/release.js`. It will push a docker image tagged with the version. It will 31 | also create a git tag for that version, with the "v" prefix, and push it up to Github. 32 | 33 | ### Update Component.json 34 | 35 | Increment the version in `component.json` so that release candidates will be built with 36 | the next anticipated version in their tag name. Commit change. -------------------------------------------------------------------------------- /docs/extensions/existing.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: existing 3 | title: Existing Extensions 4 | sidebar_label: Existing Extensions 5 | --- 6 | 7 | This is the current list of extensions defined by the repository code. 8 | 9 | ## Network Types 10 | 11 | - **LoRa** - The LoRa Network Type, defines the data used for LoRa networks as 12 | defined by the [LoRa Alliance](https://www.lora-alliance.org/), especially 13 | as implemented by the [ChirpStack](https://www.chirpstack.io) project. 14 | 15 | - **IP** - This Network Type represents all IP-based devices, such as mobile 3GPP devices. 16 | 17 | ## Network Protocols 18 | 19 | - **ChirpStack** - The network protocol API defined by the 20 | [ChirpStack](https://www.chirpstack.io) project created by Orne Brocaar. 21 | 22 | - **The Things Network** - The network protocol API defined by 23 | [The Things Network](https://www.thethingsnetwork.org/). 24 | 25 | - **Loriot** - The network protocol API defined by 26 | [Loriot](https://www.loriot.io/). 27 | 28 | - **IP** - The network protocol for IP-based devices. 29 | 30 | ## Reporting Protocols 31 | 32 | - **POST** - A basic HTTP POST protocol that sends the data received from the 33 | remote network to the Application's configured server. 34 | -------------------------------------------------------------------------------- /docs/guides/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting-started 3 | title: Getting Started 4 | sidebar_label: Getting Started 5 | --- 6 | 7 | In this guide, we'll start an instance of LPWAN Server, along with a Postgresql 8 | database. We'll also run an instance of [ChirpStack](https://www.chirpstack.io). 9 | We'll add the instance of ChirpStack within the LPWAN Server web-client UI, 10 | which will cause the applications and devices to be pulled. 11 | 12 | This guide involves running multiple services. We'll use 13 | [Docker Compose](https://docs.docker.com/compose/) to run the databases 14 | and services. 15 | 16 | ## Setup 17 | 18 | Docker, Docker Compose, and Node.js are required to run LPWAN Server 19 | according to this guide. 20 | 21 | Refer to the [Requirements page](install/requirements) 22 | for instructions on how to install these tools. 23 | 24 | Refer to the [Download and Setup page](/install/download) 25 | for instructions on downloading and installing LPWAN Server. 26 | 27 | ## Demo 28 | 29 | The sequence of steps involved for starting and stopping the services are sequenced 30 | in a bash script at `/bin/demo` within the LPWAN Server repo. You can view these 31 | files to find out more about what commands and configurations are involved in 32 | running the demo. 33 | 34 | - `/bin/demo` 35 | - `/development/databases/docker-compose.yml` 36 | - `/development/chirpstack/docker-compose.yml` 37 | - `/docker-compose.yml` 38 | 39 | ## Start Demo 40 | 41 | The LPWAN Server is setup with a "demo" script to start the server and accompanying services. 42 | Running the demo is the easiest way to try out LPWAN Server. 43 | 44 | ``` 45 | # Start demo 46 | ./bin/demo 47 | 48 | # Stop demo 49 | ./bin/demo stop 50 | ``` 51 | 52 | ### URLs 53 | 54 | - LPWAN Server REST API - https://localhost:3200/api 55 | - LPWAN Server Web Client - https://localhost:3200 56 | - ChirpStack App Server - http://localhost:8082 57 | - ChirpStack App Server V1 - https://localhost:8081 58 | 59 | ## Use Demo 60 | 61 | ### Login 62 | 63 | Use these credentials to log in. 64 | 65 | - **username** - `admin` 66 | - **password** - `password` 67 | 68 | ### Create a connection to the ChirpStack Network 69 | 70 | * Click the `Networks` link in the top navigation bar 71 | * Click on the `CREATE` button next to the LoRa Server entry 72 | * Fill in the form as shown below, and hit `SUBMIT` 73 | - Network Name: **Lora NW** 74 | - Network Base URL: **http://chirpstack_app_svr:8080/api** 75 | - Username: **admin** 76 | - Password: **admin** 77 | 78 | If the network was succesfully created, this confirms that LPWAN Server and ChirpStack are communicating correctly. 79 | 80 | ### View Application 81 | 82 | If you navigate to the home page, you will see an application that was pulled from the ChirpStack network. 83 | -------------------------------------------------------------------------------- /docs/install/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: build 3 | title: Build 4 | sidebar_label: Build 5 | --- 6 | 7 | ## LPWAN Server 8 | 9 | If you've customized the LPWAN Server, you can build a custom docker image. 10 | This shouldn't be necessary unless you've actually added or modified code. 11 | It would also be necessary if you want LPWAN Server to serve a custom UI. 12 | 13 | Run docker build: 14 | 15 | ``` 16 | $ docker build -f Dockerfile -t /: . 17 | ``` 18 | 19 | ## Web Client 20 | 21 | If you want to use the default web-client, but you want to customize or configure it, 22 | you will need to build the static assets. 23 | 24 | ``` 25 | # From within the lpwanserver-web-client repo 26 | npm run build 27 | ``` 28 | 29 | If you plan to serve the customized/configured web-client from LPWAN Server, 30 | you'll need to copy the built files into the lpwanserver repo and then use the command above 31 | to build a new docker image. The Dockerfile is setup to copy the `public` folder into the image, 32 | so it's easiest to use the `public` folder. Also make sure the LPWAN Server's `public_dir` 33 | config variable is set to `public`. 34 | 35 | ``` 36 | mv build/* ../lpwanserver/app/generated/public 37 | # run docker build command above 38 | ``` 39 | 40 | If you plan to serve the static assets from elsewhere, you'll need to set the location 41 | of the LPWAN Server before building the web-client assets. Use `REACT_APP_REST_SERVER_URL` 42 | in the `.env` file. 43 | -------------------------------------------------------------------------------- /docs/install/deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: deployment 3 | title: Deployment 4 | sidebar_label: Deployment 5 | --- 6 | 7 | ## Deployment 8 | 9 | A public docker image exists for 10 | [LPWAN Server](https://hub.docker.com/r/lpwanserver/lpwanserver/). 11 | The easiest way to deploy LPWAN Server is to use a container management tool, like 12 | [Docker Machine](https://docs.docker.com/machine/overview/). LPWAN Server provides 13 | [an example docker-compose file](https://github.com/cablelabs/lpwanserver/blob/master/docker/docker-compose.yml). 14 | 15 | ### Database 16 | 17 | LPWAN Server depends on a running instance of 18 | [Prisma](https://prisma.io) to which the provided Datamodel has been deployed. 19 | The datamodel is at `prisma/datamodel.prisma`. 20 | Prisma provides a Docker image, so you can run Prisma 21 | alongside LPWAN Server. Prisma gets configured with the details 22 | of Postgresql. You can choose to run Postgres via the public 23 | Postgres Docker image or by using a hosted Postgres service. 24 | 25 | #### Setup Postgresql 26 | 27 | Setup an uninitialized Postgres service. 28 | 29 | #### Setup Prisma 30 | 31 | Make a folder inside the repo at `prisma/prod` to hold your production `prisma.yml` file. 32 | Create or update a running Prisma instance to be able to connect to your Postgres instance. 33 | Here are some documentation pages. 34 | 35 | - [Prisma Server](https://www.prisma.io/docs/prisma-server/) 36 | 37 | Make sure to setup Authentication by following the docs. 38 | 39 | From within your production `prisma.yml` file, point to the datamodel. 40 | 41 | #### Deploy Prisma 42 | 43 | Use Prisma's CLI to deploy the provided datamodel to the Prisma service that you created 44 | for LPWAN Server. 45 | 46 | #### Redis 47 | 48 | LPWAN Server uses Redis for PubSub and DB caching. 49 | 50 | #### Configure LPWAN Server to connect to Prisma and Redis 51 | 52 | Set these environment variables when running LPWAN Server. 53 | 54 | - **prisma_url** - url to your Prisma service 55 | - **redis_url** - url to your Redis service 56 | 57 | ### LPWAN Server 58 | 59 | [Docker Image](https://hub.docker.com/r/lpwanserver/lpwanserver/) 60 | 61 | Refer to the 62 | [configuration page](configuration) 63 | for information on configuring the LPWAN Server. 64 | 65 | ### Web Client Assets 66 | 67 | If you want to serve the web-client from a CDN, object storage platform, or 68 | any other server than the one provided in the Web Client docker image, the 69 | [build page](build) 70 | shows you how to configure the Web Client with the LPWAN Server location 71 | and build the app assets. Also set LPWAN Server's `public_dir` setting 72 | to an empty string when deploying LPWAN Server. 73 | -------------------------------------------------------------------------------- /docs/install/download.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: download 3 | title: Download and Setup 4 | sidebar_label: Download and Setup 5 | --- 6 | 7 | The **LPWAN Server** project consists of 2 Git repositories. 8 | 9 | ## LPWAN Server 10 | 11 | The LPWAN Server is written in Javascript using Node.js. See the 12 | [LPWAN Server Git repository](https://github.com/cablelabs/lpwanserver.git) 13 | for more information. 14 | 15 | Run these commands to download and install LPWAN Server. 16 | 17 | ``` 18 | $ git clone https://github.com/cablelabs/lpwanserver.git 19 | $ cd lpwanserver 20 | $ npm install 21 | ``` 22 | 23 | ### Certificates 24 | 25 | LPWAN Server must use TLS. The repository contains a script for generating self-signed 26 | certificates for running the LPWAN Server locally. You will see a browser warning 27 | when using the UI. Click "advanced" and "proceed anyway" to get to the UI. To avoid 28 | seeing that message, you can also import `certs/ca-crt.pem` into your browser's certificates 29 | management settings. 30 | 31 | Run these commands to generate self-signed certificates for development. 32 | 33 | ``` 34 | mkdir certs 35 | ./development/bin/generate-development-certificates 36 | ``` 37 | 38 | ## LPWAN Server Web Client 39 | 40 | It is not necessary to clone the web client repository unless you are developing the LPWAN Server. 41 | The docker image for LPWAN Server includes the UI application. 42 | Skip this section if you are only evaluating LPWAN Server. 43 | 44 | The web client is a [React](https://github.com/facebook/react) application. See the 45 | [web client Git repository](https://github.com/cablelabs/lpwanserver-web-client.git) 46 | for more information. 47 | 48 | Run these commands to download and install LPWAN Server web client. 49 | 50 | ``` 51 | $ git clone https://github.com/cablelabs/lpwanserver-web-client.git 52 | $ cd lpwanserver-web-client 53 | $ npm install 54 | ``` 55 | 56 | ## Executable Scripts 57 | 58 | Both the REST server and the web client have a `bin` directory with scripts to make certain 59 | workflows more efficient. If you encounter permissions errors in running the scripts, try 60 | running these commands from the relavant repository. 61 | 62 | ``` 63 | $ sudo chown $USER bin/* 64 | $ sudo chmod 755 bin/* 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/install/requirements.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: requirements 3 | title: Requirements 4 | sidebar_label: Requirements 5 | --- 6 | 7 | ## Docker and Docker-Compose 8 | 9 | The development of LPWAN Server is largely based on 10 | [Docker](https://docker.com). 11 | Docker and Docker-Compose are required to run the [Getting Started guide](guides/gettings-started) 12 | or run the ChirpStack Servers for development and testing. 13 | 14 | ### Install Docker 15 | 16 | Please refer to the 17 | [Get Started with Docker](https://www.docker.com/get-started) 18 | guide to install Docker for MacOS or Windows. When installing Docker 19 | on Linux, please refer to one of the following guides: 20 | 21 | - [CentOS](https://docs.docker.com/install/linux/docker-ce/centos/#install-docker-ce) 22 | - [Debian](https://docs.docker.com/install/linux/docker-ce/debian/) 23 | - [Fedora](https://docs.docker.com/install/linux/docker-ce/fedora/) 24 | - [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/) 25 | 26 | ### Install Compose 27 | 28 | To install Docker Compose on Linux, please refer to the 29 | [Install Compose on Linux systems](https://docs.docker.com/compose/install/#install-compose) 30 | guide. You can skip this step for MacOS and Windows. 31 | 32 | ## Git 33 | 34 | ``` 35 | $ apt install git 36 | ``` 37 | 38 | ## Node.js 39 | 40 | For development, the system should have Node v8.3.0 or higher and NPM v5.6.0 or higher. 41 | We recommend installing Node.js and NPM through a version manager. 42 | See the [NPM Docs page on Installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 43 | -------------------------------------------------------------------------------- /docs/install/run.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: run 3 | title: Running LPWAN Server for Development 4 | sidebar_label: Run 5 | --- 6 | 7 | Developing LPWAN Server involves running other services along with LPWAN Server. 8 | Development is based on Docker. There are docker-compose files for each 9 | group of services that need to run, and they're set up to network 10 | with each other. 11 | 12 | ## Prisma, Postgresql, and Redis 13 | All persistence services are contained in one docker-compose file. These are used 14 | by LPWAN Server and the ChirpStack instance used in development. 15 | There is a script for making it easier to start and stop persistence services. 16 | 17 | Before starting the LPWAN Server for development, you must first start the database 18 | and then deploy the models. The `deploy` command, listed below, generates the database 19 | client, which is required by the LPWAN server. 20 | 21 | ``` 22 | # Start all persistence services 23 | $ ./development/bin/manage-db start 24 | 25 | # Deploy the data models to Prisma 26 | $ ./development/bin/manage-db deploy 27 | 28 | # Stop all persistence services 29 | $ ./development/bin/manage-db stop 30 | 31 | # Reset the database to the base set of seeded records contained in `prisma/verions/v{x}` 32 | $ ./development/bin/manage-db reset 33 | ``` 34 | 35 | Prisma runs a GraphQL Playground at `http://localhost:4466` that allows you 36 | to query and mutate the database. You can also deploy it with a setting that enables 37 | a UI dashboard to the data. 38 | 39 | ## ChirpStack 40 | 41 | Run this command to start versions 1 and 2 of ChirpStack and ChirpStack App Server. 42 | 43 | ``` 44 | $ docker-compose -f development/chirpstack/docker-compose.yml up 45 | ``` 46 | 47 | ChirpStack App Servers are at: 48 | 49 | - `https://localhost:8081` (v1) 50 | - `http://localhost:8082` (v2) 51 | 52 | ## LPWAN Server 53 | 54 | Run this command to start LPWAN Server for development. 55 | 56 | ``` 57 | $ docker-compose -f development/docker-compose.yml up --build 58 | ``` 59 | 60 | LPWAN Server is run with `nodemon` inside of a docker container. Any 61 | changes within the `rest` folder will cause the server to restart. 62 | Any changes outside of the rest folder will require restarting the container. 63 | 64 | The LPWAN Server REST API is at `https://localhost:3200/api`. 65 | 66 | ## Web Client 67 | 68 | If you're developing the server and you need the UI, run this command from within 69 | the lpwanserver-web-client repo to start the web client for development. 70 | 71 | ``` 72 | $ npm run dev 73 | ``` 74 | 75 | The web-client is at `http://localhost:3000`. 76 | 77 | [Webpack Development Server](https://github.com/webpack/webpack-dev-server) 78 | will update the browser on changes to the web-client source code. 79 | 80 | ## Docker Networking 81 | 82 | The services network through Docker, so when adding a ChirpStack network in LPWAN Server, 83 | you need to use the internal URL as the baseUrl. 84 | 85 | - `https://chirpstack_app_svr_1:8080/api` (v1) 86 | - `http://chirpstack_app_svr:8080/api` (v2) 87 | -------------------------------------------------------------------------------- /docs/openapi/endpoints/session.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.2.1 4 | title: An include file to define Session endpoints 5 | license: 6 | name: Apache 2.0 7 | paths: 8 | /api/sessions: 9 | post: 10 | operationId: createSession 11 | summary: Create a Session 12 | description: Submit a user's credentials and receive a JWT token 13 | parameters: [] 14 | security: [] 15 | tags: 16 | - Session 17 | requestBody: 18 | $ref: '#/components/requestBodies/LoginCredentials' 19 | responses: 20 | '200': 21 | description: JWT as response body 22 | content: 23 | text/plain: 24 | schema: 25 | type: string 26 | '401': 27 | description: Unauthorized 28 | '400': 29 | description: Bad Request 30 | delete: 31 | operationId: deleteSession 32 | summary: Delete a Session 33 | description: Invalidate the user's current jwt token 34 | parameters: [] 35 | security: 36 | - bearer_token: [] 37 | tags: 38 | - Session 39 | responses: 40 | '204': 41 | description: No content 42 | components: 43 | requestBodies: 44 | LoginCredentials: 45 | content: 46 | application/json: 47 | schema: 48 | type: object 49 | required: 50 | - login_username 51 | - login_password 52 | properties: 53 | login_username: 54 | type: string 55 | login_password: 56 | type: string 57 | description: User's login credentials 58 | required: true 59 | -------------------------------------------------------------------------------- /docs/overview/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: features 3 | title: Features 4 | sidebar_label: Features 5 | --- 6 | 7 | ## Support many IoT networks in one place 8 | 9 | There are many network providers out there, each with their own preferred 10 | technologies. **LPWAN Server** allows the client application vendor to 11 | configure their application(s) and devices in a single interface for any number 12 | of these network providers at one time, and in one place. 13 | 14 | ## Support many types of IoT networks 15 | 16 | Technologies for IoT devices can vary, from 17 | LoRa ([The LoRa Alliance](https://www.lora-alliance.org)) to 18 | NB-IoT ([Wikipedia](https://en.wikipedia.org/wiki/NarrowBand_IOT)) to many more. 19 | **LPWAN Server** allows for the unique configuration needs of the technologies 20 | without requiring the redundant and tedious and error-prone re-entry of the 21 | data across many interfaces. 22 | 23 | ## Data delivery consolidation 24 | 25 | The data from the various networks is delivered to the **LPWAN Server**, which 26 | then passes that data back to the client application vendor's system. 27 | 28 | ## Extendable 29 | 30 | **LPWAN Server** was designed to be extendable. Of course, new networks can be 31 | added as needed, but also new network types (LoRa vs. NB-IoT, etc.) and network 32 | configuration protocols (LoRa Open Source vs. The Things Network) can be added 33 | as needed by the **LPWAN Server** system administrators. Further, the user 34 | interface can be easily extended to support the new data required by these new 35 | network types. 36 | -------------------------------------------------------------------------------- /docs/ui/networks.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: networks 3 | title: Networks 4 | sidebar_label: Networks 5 | --- 6 | 7 | ## Network Type Management 8 | 9 | Network Types can be viewed and updated by selecting the Networks pull-down 10 | from the top menu, and selecting the Network Types item. 11 | 12 | A selection list of existing Network Types is provided. To edit one, simply 13 | click on the name. To create a new one, click the "CREATE NETWORK TYPE" 14 | button. 15 | 16 | From the **LPWAN Server UI**'s perspective, Network Type is a very simple thing. 17 | It's just a name. That's it. 18 | 19 | But that name is *very* important. It gets tied to UI customization code. It 20 | tells us what data to expect for a particular network. For the most part, even a 21 | system administrator would not be adding a new Network Type without working with 22 | a developer who can make the Network Type name relate to the appropriate software 23 | in the system. But the ability to add or remove them is provided so irrelevant 24 | (or newly relevant) Network Types can be removed (or added) to the system. 25 | 26 | ## Network Provider Management 27 | 28 | Network Providers can be viewed and updated by selecting the Networks pull-down 29 | from the top menu, and selecting the Network Providers item. 30 | 31 | A selection list of existing Network Providers is provided. To edit one, simply 32 | click on the name. To create a new one, click the "CREATE NETWORK PROVIDER" 33 | button. 34 | 35 | The purpose of the Network Provider is to make it easy to determine who is 36 | responsible for the specific remote network or group of networks. 37 | 38 | ## Network Management 39 | 40 | Networks can be viewed and updated by selecting the Networks pull-down 41 | from the top menu, and selecting the Networks item. 42 | 43 | A selection list of existing Networks is provided. To edit one, simply 44 | click on the name. To create a new one, click the "CREATE NETWORK" 45 | button. 46 | 47 | Networks link a Network Type (defining the data needed for the network), a 48 | Network Protocol (defining how to interact with the remote network server), 49 | and a Network Provider (who is responsible for the server). In addition, a 50 | Base URL is provided which is passed to the Network Protocol so the code can 51 | know how to find the remote network server on the Internet. Finally, any 52 | Network Type-specific configuration is set. 53 | 54 | Note that when a Network Type is selected, only those Network Protocols that 55 | support the Network Type will be enabled for selection. 56 | 57 | Base URLs *should* use https, or the network should be implemented to encrypt 58 | data to the remote server so no sensitive data can be easily intercepted. 59 | 60 | --- 61 | Network Form 62 | -------------------------------------------------------------------------------- /docs/ui/users.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: users 3 | title: Users and Authentication 4 | sidebar_label: Users and Authentication 5 | --- 6 | 7 | ## User Roles 8 | 9 | The **LPWAN Server UI** supports management of the **LPWAN Server** by 10 | system administrators. Non-admin users are able to manage applications 11 | and devices. 12 | 13 | System administrators are users that have been designated 14 | as "Admin". Logging in as a system administrator enables a menu at the top of 15 | the screen that allows for provisioning various aspects of the system. They 16 | can also see the applications, devices, etc. created by all users. 17 | 18 | Any errors detected by the system will display in a red box at the top of the 19 | screen. Some of these errors may reflect errors in working with a remote 20 | network. For these errors, it is usually best to return to the UI at a later 21 | time and "Push" the data you updated to the remote server. 22 | 23 | For more details, please use the menu to the left to select your area of 24 | interest. 25 | 26 | ## Login 27 | 28 | User logins occur much as they do on any other system. Enter your username and 29 | password and hit "SUBMIT". 30 | 31 | The default initial setup will have a username "admin" with the password "password". 32 | This password should be changed upon first login. User the pull-down on the 33 | username in the top menu, and select "Change Profile". By default, the field 34 | for changing a password is not available (to prevent accidental changes). Click 35 | the "CHANGE PASSWORD" button to enable the field, and enter the new password. 36 | 37 | ## User Management 38 | 39 | Users log in to and interact with the system. Users can be of 2 types: 40 | 41 | - **System Administrators** - The user's role is defined as "Admin". These 42 | Users have full access to the system, and access to additional "Admin UI" 43 | functionality. System Administrators must supply an email address so they 44 | can be notified of system events. 45 | - **Regular Users** - Non-admin users, can use the application in the context 46 | of that user. 47 | 48 | For admin users, the home screen has a tab for managing users. 49 | 50 | To get details on an User, click on the username. To create new user, click the 51 | "CREATE USER" button. 52 | 53 | --- 54 | User Form 55 | -------------------------------------------------------------------------------- /docs/use-cases/authenticate.md: -------------------------------------------------------------------------------- 1 | # Use Case - Authenticate 2 | 3 | The Authenticate use case describes the steps that occur when a user 4 | authenticates with LPWAN Server. 5 | 6 | ## Success Scenario 7 | 8 | * A: Actor, User 9 | * S: System 10 | 11 | 1. A: Post credentials to authentication endpoint 12 | 2. S: Load User 13 | 3. S: Validate password 14 | 4. S: Construct and respond with token 15 | 16 | ### System fails to load User 17 | 18 | 2. S: Fails to load User 19 | 3. S: Respond with error 20 | 21 | ### System fails to verify password 22 | 23 | 3. S: Hash of submitted password doesn't match hash in User record 24 | 4. S: Respond with error. 25 | 26 | ## Issues 27 | 28 | - Authentication system might consider blocking an IP address for 29 | a period of time after multiple failed attempts. 30 | -------------------------------------------------------------------------------- /docs/use-cases/create-network.md: -------------------------------------------------------------------------------- 1 | # Use Case - Create Network 2 | 3 | The Create Network use case describes the steps that occur when 4 | a user create's a network. 5 | 6 | ## Success Scenario 7 | 8 | * A: Actor, User 9 | * S: System 10 | 11 | 1. A: Post the data that represents the network to be created 12 | 2. S: Create Network 13 | 3. S: Authenticate with remote network 14 | 4. [Pull network](pull-network.md) 15 | 5. [Push all remote networks of same NetworkType](push-networks.md) 16 | 6. S: Respond with Network. 17 | 18 | ### System fails to create the Network 19 | 20 | 2. S: Fails to create Network 21 | 3. S: Respond with error. 22 | 23 | ### Network doesn't include authentication data 24 | 25 | 3. S: Respond with Network. 26 | 27 | ### System fails to authenticate with network 28 | 29 | 3. S: Fails to authenticate with remote network 30 | 4. S: Respond with Network. Network.securityData.message="Pending Authorization" 31 | 32 | ### System fails to pull network 33 | 34 | 4. S: Error occurs while pulling network 35 | 5. S: Respond with the error. 36 | 37 | ### System fails to push all networks 38 | 39 | 5. S: Error occurs while pushing all networks 40 | 6. S: Respond with the error. 41 | 42 | ## Issues 43 | 44 | - In the event pull or push fails, the network isn't deleted or returned to the user. 45 | -------------------------------------------------------------------------------- /docs/use-cases/create-remote-application.md: -------------------------------------------------------------------------------- 1 | # Use Case - Create Remote Application 2 | 3 | The Create Remote Application use case describes the steps that occur when 4 | the system creates one application on a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load Application 11 | 2. S: Find ApplicationNetworkTypeLink 12 | 3. S: Post request to remote network to create application 13 | 4. S: Upsert ProtocolData that contains remote application ID 14 | 15 | ### System fails to load Application 16 | 17 | 1. S: Fails to load Application 18 | 2. S: Respond with error 19 | 20 | ### System fails to find ApplicationNetworkTypeLink 21 | 22 | 2. S: Fails to find ApplicationNetworkTypeLink 23 | 3. S: Respond with error 24 | 25 | ### System fails to create application on remote network 26 | 27 | 3. S: Fails to create remote application 28 | 4. S: Respond with error 29 | 30 | ### System fails to upsert ProtocolData for remote application ID 31 | 32 | 4. S: Fails to upsert ProtocolData for remote application ID 33 | 5. S: Respond with error 34 | -------------------------------------------------------------------------------- /docs/use-cases/create-remote-device-profile.md: -------------------------------------------------------------------------------- 1 | # Use Case - Create Remote Device Profile 2 | 3 | The Create Remote Device Profile use case describes the steps that occur when 4 | the system creates one device profile on a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load Device Profile 11 | 2. S: Post request to remote network to create device profile 12 | 3. S: Upsert ProtocolData that contains remote device profile ID 13 | 14 | ### System fails to load DeviceProfile 15 | 16 | 1. S: Fails to load DeviceProfile 17 | 2. S: Respond with error 18 | 19 | ### System fails to create device profile on remote network 20 | 21 | 2. S: Fails to create remote device profile 22 | 3. S: Respond with error 23 | 24 | ### System fails to upsert ProtocolData for remote device profile ID 25 | 26 | 3. S: Fails to upsert ProtocolData for remote device profile ID 27 | 4. S: Respond with error 28 | -------------------------------------------------------------------------------- /docs/use-cases/create-remote-device.md: -------------------------------------------------------------------------------- 1 | # Use Case - Create Remote Device 2 | 3 | The Create Remote Device use case describes the steps that occur when 4 | the system creates one device on a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load Device 11 | 2. S: Find DeviceNetworkTypeLink 12 | 3. S: Load DeviceProfile 13 | 4. S: Find ProtocolData that contains remote application ID 14 | 5. S: Find ProtocolData that contains remote device profile ID 15 | 6. S: Post request to remote network to create device 16 | 7. S: Upsert ProtocolData that contains remote device ID 17 | 18 | ### System fails to load Device 19 | 20 | 1. S: Fails to load Device 21 | 2. S: Respond with error 22 | 23 | ### System fails to find DeviceNetworkTypeLink 24 | 25 | 2. S: Fails to find DeviceNetworkTypeLink 26 | 3. S: Respond with error 27 | 28 | ### System fails to load DeviceProfile 29 | 30 | 3. S: Fails to load DeviceProfile 31 | 4. S: Respond with error 32 | 33 | ### System fails to find ProtocolData for remote application ID 34 | 35 | 4. S: Fails to find ProtocolData for remote application ID 36 | 5. S: Respond with error 37 | 38 | ### System fails to find ProtocolData for remote device profile ID 39 | 40 | 5. S: Fails to find ProtocolData for remote device profile ID 41 | 6. S: Respond with error 42 | 43 | ### System fails to create device on remote network 44 | 45 | 6. S: Fails to create remote device 46 | 7. S: Respond with error 47 | 48 | ### System fails to upsert ProtocolData for remote device ID 49 | 50 | 7. S: Fails to upsert ProtocolData for remote device ID 51 | 8. S: Respond with error 52 | -------------------------------------------------------------------------------- /docs/use-cases/multi-type-devices.md: -------------------------------------------------------------------------------- 1 | # Use Case - Multi-Type Devices 2 | 3 | The Multi-Type devices use case describes a sequence of plausable 4 | steps the might occur when managing a single model device that 5 | supports multiple network types (like dual radio devices). 6 | 7 | **Network Types** 8 | * IP 9 | * LoRa, ChirpStack v2 10 | 11 | ## Steps 12 | 13 | * A: Actor, User with role "USER" 14 | * S: System 15 | 16 | 1. [Authenticate](authenticate.md) 17 | 2. [Create ChirpStack v2 network](create-network.md) 18 | 3. A: Add IP device profile 19 | 4. A: Add LoRa device profile 20 | 5. A: Create application 21 | 6. A: Create IP ApplicationNetworkTypeLink 22 | 7. A: Create LoRa ApplicationNetworkTypeLink 23 | 8. A: Confirm Application is on ChirpStack network. 24 | 9. A: Create Device 25 | 10. A: Create IP DeviceNetworkTypeLink 26 | 11. A: Create LoRa DeviceNetworkTypeLink 27 | 12: A: Confirm Device and DeviceProfile are on ChirpStack network 28 | 13: A: Send an uplink from each device 29 | 14: A: Verify application server received uplinks 30 | 15: A: Send a downlink to devices 31 | 16: A: Verify downlink received by IP device. 32 | 17: A: Verify downlink was received by ChirpStack network. 33 | 18: A: Delete LoRa DeviceNetworkTypeLink 34 | 19: A: Send a downlink to devices 35 | 20: A: Confirm msg not received by ChirpStack network. 36 | 21: A: Send an uplink from ChirpStack network. 37 | 22: A: Confirm msg doesn't reach application server. 38 | 39 | ## Issues 40 | 41 | - NetworkProtocols should declare requirements for downlinks. LPWAN should 42 | validate the downlink with all NetworkProtocols before attempting to send 43 | the message to any. 44 | -------------------------------------------------------------------------------- /docs/use-cases/pull-application.md: -------------------------------------------------------------------------------- 1 | # Use Case - Pull Application 2 | 3 | The Pull Application use case describes the steps that occur when 4 | the system pulls one application from a remote network. This doesn't 5 | include pulling devices. 6 | 7 | ## Success Scenario 8 | 9 | * S: System 10 | 11 | 1. S: Load remote application 12 | 2. S: Load remote application HTTP integration 13 | 3. S: Find local Application with the same name 14 | 4. S: Find ApplicationNetworkTypeLink 15 | 5. S: If HTTP integration found, [start Application](start-application.md) 16 | 6. S: Respond with remote and local application IDs 17 | 18 | ### System fails to load application 19 | 20 | 1. S: Error occurs while loading application 21 | 2. S: Respond with error 22 | 23 | ### System fails to load an existing remote HTTP integration 24 | 25 | 2. S: Error occurs while loading HTTP integration 26 | 3. S: Respond with error 27 | 28 | ### System fails to find a local app with the same name 29 | 30 | 3. S: Fails to find local app with the same name 31 | 4. S: Create a local Application 32 | 5. Resume at step 4 in success scenario 33 | 34 | #### System fails to create Application 35 | 36 | 4. S: Fails to create Application 37 | 5. Respond with error. 38 | 39 | ### System fails to find ApplicationNetworkTypeLink 40 | 41 | 4. S: Fails to find ApplicationNetworkTypeLink 42 | 5. S: Create ApplicationNetworkTypeLink 43 | 6. S: Create a ProtocolData entry to store the remote application's ID 44 | 7. Resume at step 5 in success scenario 45 | 46 | #### System fails to create ApplicationNetworkTypeLink 47 | 48 | 5. S: System fails to create ApplicationNetworkTypeLink 49 | 6. S: Respond with error 50 | 51 | #### System fails to create ProtocolData for remote app ID 52 | 53 | 6. S: System fails to create ProtocolData 54 | 7. S: Respond with error 55 | 56 | ### System fails to start Application 57 | 58 | 5. S: Fails to start Application 59 | 6. S: Respond with error 60 | 61 | ## Issues 62 | 63 | - Not all networks expose HTTP integration functionality at the API level. 64 | - Consider including the app's LPWAN Server integration endpoint URL in the 65 | app body. This would allow for users to set the HTTP integration out-of-band. 66 | When LPWAN Server supports auth headers for HTTP integrations, consider including 67 | an API key hash in the Application data model, so users can set the auth header. 68 | - If start-app fails, the ID of the created app is not returned, and it is still enabled. 69 | Change sequence and/or use try-catch to handle a failed app start. 70 | - Name shouldn't be used to match applications. If a remote application has a local 71 | counterpart, then the system will have a reference to the ID. When remote apps are fetched, 72 | the system should be able to find the local Application using the remote app's ID 73 | and the Network ID. 74 | -------------------------------------------------------------------------------- /docs/use-cases/pull-applications.md: -------------------------------------------------------------------------------- 1 | # Use Case - Pull Applications 2 | 3 | The Pull Applications use case describes the steps that occur when 4 | the system pulls all applications from a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Fetch all applications from remote network, limited to 9,999 11 | 2. S: For all apps in parallel, [pull application](pull-application.md) 12 | 3. S: Return an array of objects with references to the remote app ID and local app ID 13 | 14 | ### System fails to fetch a list of applications 15 | 16 | 1. S: Fails to fetch applications from remote network. 17 | 2. S: Respond with error 18 | 19 | ### System fails to pull all applications 20 | 21 | 2. S: Fails to pull all applications successfully 22 | 3. S: Respond with failed pull error 23 | 24 | ## Issues 25 | 26 | - Failure to pull any application will halt operation. 27 | - If an error occurs, the user doesn't receive feedback as to which 28 | apps were pulled. Process should use a 29 | [reflect abstraction](https://stackoverflow.com/questions/31424561/wait-until-all-es6-promises-complete-even-rejected-promises) 30 | to prevent Promise.all from rejecting. 31 | -------------------------------------------------------------------------------- /docs/use-cases/pull-device-profile.md: -------------------------------------------------------------------------------- 1 | # Use Case - Pull Device Profile 2 | 3 | The Pull Device Profile use case describes the steps that occur when 4 | the system pulls one device profile from a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load remote device profile 11 | 2. S: Find local DeviceProfile with the same name 12 | 3. S: Upsert ProtocolData for the remote device profile ID 13 | 4. S: Respond with local and remote device profile IDs 14 | 15 | ### System fails to load remote device profile 16 | 17 | 1. S: Fails to load remote device profile 18 | 2. S: Respond with error 19 | 20 | ### System fails to find local DeviceProfile with same name 21 | 22 | 2. S: Fails to find local DeviceProfile with the same name 23 | 3. S: Create DeviceProfile 24 | 4. Resume at step 3 in success scenario 25 | 26 | #### System fails to create DeviceProfile 27 | 28 | 3. S: Fails to create DeviceProfile 29 | 4. S: Respond with error 30 | 31 | ### System fails to upsert ProtocolData 32 | 33 | 3. S: Fails to upsert ProtocolData for remote device profile ID. 34 | 4. S: Respond with error 35 | 36 | ## Issues 37 | 38 | - Name shouldn't be used to match remote and local device profiles. 39 | See similar issue in [Pull Application](pull-application.md) 40 | -------------------------------------------------------------------------------- /docs/use-cases/pull-device-profiles.md: -------------------------------------------------------------------------------- 1 | # Use Case - Pull Device Profiles 2 | 3 | The Pull Device Profiles use case describes the steps that occur when 4 | the system pulls all device profiles from a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Fetch all device profiles from remote network, limited to 9,999 11 | 2. S: For all device profiles in parallel, [pull device profile](pull-device-profile.md) 12 | 3. S: Return an array of objects with references to the remote and local device profile IDs 13 | 14 | ### System fails to fetch a list of device profiles 15 | 16 | 1. S: Fails to fetch device profiles from remote network 17 | 2. S: Respond with error 18 | 19 | ### System fails to pull all device profiles 20 | 21 | 2. S: Fails to pull all device profiles successfully 22 | 3. S: Respond with failed pull error 23 | 24 | ## Issues 25 | 26 | - Many networks don't support device profiles 27 | -------------------------------------------------------------------------------- /docs/use-cases/pull-device.md: -------------------------------------------------------------------------------- 1 | # Use Case - Pull Device 2 | 3 | The Pull Device use case describes the steps that occur when 4 | the system pulls one device from a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load remote device 11 | 2. S: Load local DeviceProfile 12 | 3. S: Find local Device with the same name 13 | 4. S: Find DeviceNetworkTypeLink 14 | 5. S: Upsert ProtocolData for the remote device ID 15 | 6. S: Respond with local Device ID 16 | 17 | ### System fails to load remote device 18 | 19 | 1. S: Fails to load remote device 20 | 2. S: Respond with error 21 | 22 | ### System fails to load remote device profile 23 | 24 | 2. S: Fails to load remote device profile 25 | 3. S: Respond with error 26 | 27 | ### System fails to find local Device with same name 28 | 29 | 2. S: Fails to find local Device with the same name 30 | 3. S: Create Device 31 | 4. Resume at step 4 in success scenario 32 | 33 | #### System fails to create Device 34 | 35 | 3. S: Fails to create Device 36 | 4. S: Respond with error 37 | 38 | ### System fails to find DeviceNetworkTypeLink with same name 39 | 40 | 4. S: Fails to find DeviceNetworkTypeLink 41 | 5. S: Create DeviceNetworkTypeLink 42 | 6. Resume at step 5 in success scenario 43 | 44 | #### System fails to create DeviceNetworkTypeLink 45 | 46 | 5. S: Fails to create DeviceNetworkTypeLink 47 | 6. S: Respond with error 48 | 49 | ### System fails to upsert ProtocolData 50 | 51 | 5. S: Fails to upsert ProtocolData for remote device ID. 52 | 6. Respond with error. 53 | 54 | ## Issues 55 | 56 | - Name shouldn't be used to match remote and local device profiles. 57 | See similar issue in [Pull Application](pull-application.md) 58 | -------------------------------------------------------------------------------- /docs/use-cases/pull-devices.md: -------------------------------------------------------------------------------- 1 | # Use Case - Pull Devices 2 | 3 | The Pull Devices use case describes the steps that occur when 4 | the system pulls all devices from a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Fetch all devices from remote network, limited to 9,999 11 | 2. S: For all devices in parallel, [pull device](pull-device.md) 12 | 3. S: Return an array of local Device IDs 13 | 14 | ### System fails to fetch a list of devices 15 | 16 | 1. S: Fails to fetch devices from remote network. 17 | 2. S: Respond with error 18 | 19 | ### System fails to pull all devices 20 | 21 | 2. S: Fails to pull all device profiles successfully 22 | 3. S: Respond with failed pull error 23 | 24 | ## Issues 25 | 26 | - Devices should be pulled by application, not all at once. 27 | -------------------------------------------------------------------------------- /docs/use-cases/pull-network.md: -------------------------------------------------------------------------------- 1 | # Use Case - Pull Network 2 | 3 | The Pull Network use case describes the steps that occur when 4 | the system pulls a remote network. Pulling a network can be 5 | initiated by the Actor via a REST call, or by the System as 6 | part of creating a Network. 7 | 8 | ## Success Scenario 9 | 10 | * A: Actor 11 | * S: System 12 | 13 | 1. A: Post a request for pulling a network 14 | 2. S: Verify that the network is authorized (create-network entry) 15 | 3. [Pull applications](pull-applications.md) and [device profiles](pull-device-profiles.md) in parallel 16 | 4. [Pull devices](pull-devices.md) (create-network exit) 17 | 5. S: Respond with list of logs from networks. 18 | 19 | ### Network is not authorized 20 | 21 | 1. S: Fails to verify that Network is authorized 22 | 2. S: Respond with an error 23 | 24 | ### System fails to pull all applications and device profiles 25 | 26 | 2. S: Error occurs while pulling all applications and device profiles 27 | 3. S: Respond with error 28 | 29 | ### System fails to pull all devices 30 | 31 | 3. S: Error occurs while pulling devices 32 | 4. S: Respond with error 33 | 34 | ## Issues 35 | 36 | - The logic for pulling a network doesn't attempt to authenticate with the network if 37 | it is not already authorized. 38 | - The sequence of pulling a network varies according to the network. For instance, on ChirpStack, 39 | a company is created on the remote network if it doesn't exist. Also, for ChirpStack, device profiles 40 | are pulled, but other networks don't have the concept of device profiles. 41 | - Network protocols should not create companies and users on the remote network. 42 | - The NetworkProtocolAPI `networkProtocols.js` simply proxies the call to pull the network. 43 | The Network model should call the pullNetwork method on the protocol directly. Same with 44 | most methods on the NetworkProtocolAPI. Since sessions were moved to the protocol clients, 45 | NetworkProtolAPI mainly just proxies calls. 46 | - The amount of successful asyncronous steps required to successfully pull a network is too big. 47 | There should be REST endpoints for pulling a device, an application (no devices), and all 48 | devices in an application. LPWAN shouldn't attempt to pull entire networks. Endpoints such as 49 | `/networks/:networkId/applications` and `/network/:networkId/applications/:applicationId/devices` 50 | could act as proxies to remote networks, providing the user with the info needed for more selective 51 | pulling. 52 | - "Pull" doesn't have consistent meaning in LPWAN Server. Pulling a network is inclusive of all apps 53 | and devices, but pulling all applications is separate from pulling all devices, and pulling 54 | all devices is separate from pulling device profiles. 55 | -------------------------------------------------------------------------------- /docs/use-cases/push-application.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Application 2 | 3 | The Push Application use case describes the steps that occur when 4 | the system pushes one application to a remote network. This doesn't 5 | include pushing devices. 6 | 7 | ## Success Scenario 8 | 9 | * S: System 10 | 11 | 1. S: Find ProtocolData that contains remote application ID 12 | 2. S: [Update remote application](update-remote-application.md) 13 | 3. S: Respond with local and remote app IDs 14 | 15 | ### System fails to find ProtocolData 16 | 17 | 1. S: Fails to find ProtocolData that contains remote app ID 18 | 2. S: [Create remote application](create-remote-application.md) 19 | 3. Resume at step 3 in success scenario 20 | 21 | #### System fails to create remote application 22 | 23 | 2. S: Fails to create remote application 24 | 3. Respond with error. 25 | 26 | ### System fails to update remote application 27 | 28 | 2. S: Fails to update remote application 29 | 3. S: Respond with error 30 | -------------------------------------------------------------------------------- /docs/use-cases/push-applications.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Applications 2 | 3 | The Push Applications use case describes the steps that occur when 4 | the system pushes all applications to a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Fetch all local applications 11 | 2. S: For all apps in parallel, [push application](push-application.md) 12 | 13 | ### System fails list local applications 14 | 15 | 1. S: Fails to list local applications 16 | 2. S: Respond with error 17 | 18 | ### System fails to push all applications 19 | 20 | 2. S: Fails to push all applications successfully 21 | 3. S: Respond with failed push error 22 | 23 | ## Issues 24 | 25 | - Failure to push any application will halt operation. 26 | - If an error occurs, the user doesn't receive feedback as to which 27 | apps were pushed. Process should use a 28 | [reflect abstraction](https://stackoverflow.com/questions/31424561/wait-until-all-es6-promises-complete-even-rejected-promises) 29 | to prevent Promise.all from rejecting. 30 | -------------------------------------------------------------------------------- /docs/use-cases/push-device-profile.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Device Profile 2 | 3 | The Push Device Profile use case describes the steps that occur when 4 | the system pushes one device profile to a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Find ProtocolData that contains remote device profile ID 11 | 2. S: [Update remote device profile](update-remote-device-profile.md) 12 | 3. S: Respond with local and remote device profile IDs 13 | 14 | ### System fails to find ProtocolData 15 | 16 | 1. S: Fails to find ProtocolData that contains remote device profile ID 17 | 2. S: [Create remote device-profile](create-remote-device-profile.md) 18 | 3. Resume at step 3 in success scenario 19 | 20 | #### System fails to create remote device profile 21 | 22 | 2. S: Fails to create remote device profile 23 | 3. Respond with error. 24 | 25 | ### System fails to update remote device profile 26 | 27 | 2. S: Fails to update remote device profile 28 | 3. S: Respond with error 29 | -------------------------------------------------------------------------------- /docs/use-cases/push-device-profiles.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Device Profiles 2 | 3 | The Push Device Profiles use case describes the steps that occur when 4 | the system pushes all device profiles to a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Fetch all local device profiles for the Network's NetworkType 11 | 2. S: For all device profiles in parallel, [push device profile](push-device-profile.md) 12 | 13 | ### System fails list local device profiles 14 | 15 | 1. S: Fails to list local applications 16 | 2. S: Respond with error 17 | 18 | ### System fails to push all device profiles 19 | 20 | 2. S: Fails to push all device profiles successfully 21 | 3. S: Respond with failed push error 22 | 23 | ## Issues 24 | 25 | - System currently lists all local device profiles, instead of restricting 26 | them by the NetworkType. 27 | -------------------------------------------------------------------------------- /docs/use-cases/push-device.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Device 2 | 3 | The Push Device use case describes the steps that occur when 4 | the system pushes one device to a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Find ProtocolData that contains remote device ID 11 | 2. S: [Update remote device](update-remote-device.md) 12 | 3. S: Respond with local and remote device IDs 13 | 14 | ### System fails to find ProtocolData 15 | 16 | 1. S: Fails to find ProtocolData that contains remote app ID 17 | 2. S: [Create remote device](create-remote-device.md) 18 | 3. Resume at step 3 in success scenario 19 | 20 | #### System fails to create remote device 21 | 22 | 2. S: Fails to create remote device 23 | 3. Respond with error. 24 | 25 | ### System fails to update remote device 26 | 27 | 2. S: Fails to update remote device 28 | 3. S: Respond with error 29 | -------------------------------------------------------------------------------- /docs/use-cases/push-devices.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Devices 2 | 3 | The Push Devices use case describes the steps that occur when 4 | the system pushes all devices to a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Fetch all local devices 11 | 2. S: For all devices in parallel, [push device](push-device.md) 12 | 13 | ### System fails list local Devices 14 | 15 | 1. S: Fails to list local Devices 16 | 2. S: Respond with error 17 | 18 | ### System fails to push all devices 19 | 20 | 2. S: Fails to push all devices successfully 21 | 3. S: Respond with failed push error 22 | 23 | ## Issues 24 | 25 | - Failure to push any device will halt operation. 26 | - If an error occurs, the user doesn't receive feedback as to which 27 | devices were pushed. Process should use a 28 | [reflect abstraction](https://stackoverflow.com/questions/31424561/wait-until-all-es6-promises-complete-even-rejected-promises) 29 | to prevent Promise.all from rejecting. 30 | - Query for local Devices should instead be a query for DeviceNetworkTypeLinks for Network's 31 | NetworkType. Get the device IDs from the DeviceNetworkTypeLinks, and push only those. 32 | - Pushed devices should be batched by application. 33 | -------------------------------------------------------------------------------- /docs/use-cases/push-network.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Network 2 | 3 | The Push Network use case describes the steps that occur when 4 | the an Actor or the System pushes a single network. 5 | 6 | ## Success Scenario 7 | 8 | * A: Actor 9 | * S: System 10 | 11 | 1. A: Post request to push a Network 12 | 2. S: Load Network (create-network entry) 13 | 3. [Push applications](push-applications.md) and [device profiles](push-device-profiles.md) in parallel 14 | 4. [Push devices](push-devices.md) (create-network exit) 15 | 5. S: Respond with list of logs from networks. 16 | 17 | ### System fails to push all applications and device profiles sucessfully 18 | 19 | 3. S: Fails to push all applications and device profiles successfully 20 | 4. S: Respond with error 21 | 22 | ### System fails to push all devices successfully 23 | 24 | 4. S: Fails to push all devices successfully 25 | 5. S: Respond with error 26 | 27 | ## Issues 28 | 29 | - System does not verify that the Network is authorized before pushing. 30 | -------------------------------------------------------------------------------- /docs/use-cases/push-networks.md: -------------------------------------------------------------------------------- 1 | # Use Case - Push Networks 2 | 3 | The Push Networks use case describes the steps that occur when 4 | the system pushes all networks of one NetworkType. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Query Networks by NetworkType 11 | 2. S: In parallel, [push each Network](push-network.md) 12 | 13 | ### System fails to find Networks with NetworkType 14 | 15 | 1. S: Fails to find Networks 16 | 2. S: Respond with error. 17 | 18 | ### System fails to push all networks successfully 19 | 20 | 2. S: Fails to push all networks 21 | 3. S: Respond with error 22 | -------------------------------------------------------------------------------- /docs/use-cases/start-application.md: -------------------------------------------------------------------------------- 1 | # Use Case - Start Application 2 | 3 | The Start Application use case describes the steps that occur when 4 | the user or system starts an application. This use case can be 5 | initiated by the user, or by the system as part of pulling an application. 6 | 7 | When the Actor initiates the use case, all errors that occur after step 6 8 | are caught and included in the logs that are the response to the Actor. 9 | 10 | ## Success Scenario 11 | 12 | * A: Actor, User 13 | * S: System 14 | 15 | 1. A: Post request to start-application endpoint 16 | 2. S: Load Application 17 | 3. S: Verify that Application has baseUrl 18 | 4. S: Update application record with enabled=true 19 | 5. S: Find ApplicationNetworkTypeLinks 20 | 6. S: In parallel, call "startApplication" for each network of each linked NetworkType 21 | 7. S: Find ProtocolData with remote application ID 22 | 8. S: Load remote HTTP integration (pull-app entry) 23 | 9. S: Update remote HTTP integration (pull-app exit) 24 | 10. S: Respond to step 1 request with logs from each network 25 | 26 | ### System fails to load Application 27 | 28 | 2. S: Error occurs on load Application 29 | 3. S: Respond with error. 30 | 31 | ### Application is missing baseUrl 32 | 33 | 3. S: Fails to verify that Application has a baseUrl 34 | 4. S: Respond with error. 35 | 36 | ### System fails to update Application 37 | 38 | 4. S: Fails to update Application with enabled=true 39 | 5. S: Respond with error. 40 | 41 | ### System fails to find ApplicationNetworkTypeLinks 42 | 43 | 5. S: Fails to query for ApplicationNetworkTypeLinks 44 | 6. S: Respond with error. 45 | 46 | ### System fails to find ProtocolData 47 | 48 | 7. S: Fails to find ProtocolData with remote application ID 49 | 8. S: Return NotFound error 50 | 51 | ### System fails to load remote HTTP integration 52 | 53 | 8. S: Fails to load remote HTTP integration 54 | 9. S: Create remote HTTP integration 55 | 56 | #### System fails to create remote HTTP integration 57 | 58 | 9. S: Fails to create remote HTTP integration 59 | 10. S: Respond with error 60 | 61 | ### System fails to update remote HTTP integration 62 | 63 | 9. S: Fails to update remote HTTP integration 64 | 10. S: Respond with error 65 | 66 | ## Issues 67 | -------------------------------------------------------------------------------- /docs/use-cases/transfer-lora-network.md: -------------------------------------------------------------------------------- 1 | # Use Case - LoRa Network Transfer 2 | 3 | The LoRa Network Transfer use case describes the steps that occur 4 | when transferring all applications and devices from one 5 | LoRa network to the next. 6 | 7 | **Networks** 8 | * ChirpStack V1 9 | * ChirpStack V2 10 | 11 | Testing the transfer from ChirpStack v1 to ChirpStack v2 will 12 | mainly cover the steps and data models that enable the transfer. 13 | There will be a minimal amount of data property renaming. The tests 14 | will cover the transfer of LoRaWAN 1.0 devices to a network that supports 15 | LoRAWAN 1.0 and 1.1. 16 | 17 | ## Steps 18 | 19 | * A: Actor, User with role "USER" 20 | * S: System 21 | 22 | 1. [Authenticate](authenticate.md) 23 | 2. [Create ChirpStack v1 network](create-network.md) 24 | 3. [Create ChirpStack v2 network](create-network.md) 25 | 26 | ### Authentication fails 27 | 28 | 1. S: Authentication fails 29 | 2. Response determined by Authentication use case 30 | 31 | ### Create ChirpStack v1 Network Fails 32 | 33 | 2. S: Fails to create Network (includes pull/push) 34 | 3. S: Response determined by Create Network use case 35 | 36 | ### Create ChirpStack v2 Network Fails 37 | 38 | 3. S: Fails to create Network (includes pull/push) 39 | 4. S: Response determined by Create Network use case 40 | 41 | ## Result 42 | 43 | All Applications, Device Profiles, and Devices are transferred, and property values were 44 | correct when inspected. 45 | 46 | ## Issues 47 | 48 | - No one-way transfer. All apps and devices on the destination network will be pushed to the source network. 49 | - No ability to choose apps to be transferred. 50 | - Transfer happens implicitly when 2 networks are added. 51 | Explicit transfer interface would be more appropriate, once one-way transfer is supported. 52 | - 1 Actor's action can map to several (several!) System actions, any of which could fail 53 | and leave the transfer in a partially fulfilled state, with no course prescribed for 54 | how to fix. A "Deployment" resource would make actions more atomic, and provide insight 55 | into network sync issues. 56 | -------------------------------------------------------------------------------- /docs/use-cases/update-remote-application.md: -------------------------------------------------------------------------------- 1 | # Use Case - Update Remote Application 2 | 3 | The Update Remote Application use case describes the steps that occur when 4 | the system updates one application on a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load Application 11 | 2. S: Find ProtocolData that contains remote application ID 12 | 3. S: Find ApplicationNetworkTypeLink 13 | 4. S: Post request to remote network to update application 14 | 15 | ### System fails to load Application 16 | 17 | 1. S: Fails to load Application 18 | 2. S: Respond with error 19 | 20 | ### System fails to find ProtocolData for remote application ID 21 | 22 | 2. S: Fails to find ProtocolData for remote application ID 23 | 3. S: Respond with error 24 | 25 | ### System fails to find ApplicationNetworkTypeLink 26 | 27 | 3. S: Fails to find ApplicationNetworkTypeLink 28 | 4. S: Respond with error 29 | 30 | ### System fails to update application on remote network 31 | 32 | 4. S: Fails to update remote application 33 | 5. S: Respond with error 34 | -------------------------------------------------------------------------------- /docs/use-cases/update-remote-device-profile.md: -------------------------------------------------------------------------------- 1 | # Use Case - Update Remote Device Profile 2 | 3 | The Update Remote Device Profile use case describes the steps that occur when 4 | the system updates one device profile on a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load Device Profile 11 | 2. S: Find ProtocolData that contains remote device profile ID 12 | 3. S: Post request to remote network to update device profile 13 | 14 | ### System fails to load DeviceProfile 15 | 16 | 1. S: Fails to load DeviceProfile 17 | 2. S: Respond with error 18 | 19 | ### System fails to find ProtocolData for remote device profile ID 20 | 21 | 2. S: Fails to find ProtocolData for remote device profile ID 22 | 3. S: Respond with error 23 | 24 | ### System fails to update device profile on remote network 25 | 26 | 3. S: Fails to update remote device profile 27 | 4. S: Respond with error 28 | -------------------------------------------------------------------------------- /docs/use-cases/update-remote-device.md: -------------------------------------------------------------------------------- 1 | # Use Case - Update Remote Device 2 | 3 | The Update Remote Device use case describes the steps that occur when 4 | the system updates one device on a remote network. 5 | 6 | ## Success Scenario 7 | 8 | * S: System 9 | 10 | 1. S: Load Device 11 | 2. S: Find DeviceNetworkTypeLink 12 | 3. S: Load DeviceProfile 13 | 4. S: Find ProtocolData that contains remote device ID 14 | 5. S: Find ProtocolData that contains remote application ID 15 | 6. S: Find ProtocolData that contains remote device profile ID 16 | 7. S: Post request to remote network to update device 17 | 18 | ### System fails to load Device 19 | 20 | 1. S: Fails to load Device 21 | 2. S: Respond with error 22 | 23 | ### System fails to find DeviceNetworkTypeLink 24 | 25 | 2. S: Fails to find DeviceNetworkTypeLink 26 | 3. S: Respond with error 27 | 28 | ### System fails to load DeviceProfile 29 | 30 | 3. S: Fails to load DeviceProfile 31 | 4. S: Respond with error 32 | 33 | ### System fails to find ProtocolData for remote device ID 34 | 35 | 4. S: Fails to find ProtocolData for remote device ID 36 | 5. S: Respond with error 37 | 38 | ### System fails to find ProtocolData for remote application ID 39 | 40 | 5. S: Fails to find ProtocolData for remote application ID 41 | 6. S: Respond with error 42 | 43 | ### System fails to find ProtocolData for remote device profile ID 44 | 45 | 6. S: Fails to find ProtocolData for remote device profile ID 46 | 7. S: Respond with error 47 | 48 | ### System fails to update device on remote network 49 | 50 | 4. S: Fails to update remote device 51 | 5. S: Respond with error 52 | -------------------------------------------------------------------------------- /lib/seeder/index.js: -------------------------------------------------------------------------------- 1 | const R = require('ramda') 2 | 3 | const omitId = R.omit(['id']) 4 | 5 | function createTypeRecord (types, typeIndex, itemIndex) { 6 | // replace relationship IDs in record 7 | let data = types[typeIndex].items[itemIndex] 8 | let rels = Object.keys(data).filter(key => { 9 | return typeof data[key] === 'object' && '$type' in data[key] 10 | }) 11 | data = rels.reduce((acc, key) => { 12 | const { $type, $id } = acc[key] 13 | const type = types.find(x => x.id === $type) 14 | if (!type) { 15 | throw new Error(`Unknown type ${$type}`) 16 | } 17 | if (!type.records || type.records.length !== type.items.length) { 18 | throw new Error(`Invalid records collection for type ${type.id}`) 19 | } 20 | const itemIndex = type.items.findIndex(x => x.id === $id) 21 | if (itemIndex < 0) { 22 | throw new Error(`Item of type ${type.id} with ID ${$id} not found.`) 23 | } 24 | acc[key] = type.records[itemIndex].id 25 | return acc 26 | }, data) 27 | return types[typeIndex].create(omitId(data)) 28 | } 29 | 30 | async function createType (types, typeIndex) { 31 | let type = types[typeIndex] 32 | let records = [] 33 | for (let i = 0; i < type.items.length; i++) { 34 | records[i] = await createTypeRecord(types, typeIndex, i) 35 | } 36 | return records 37 | } 38 | 39 | async function seedData (types) { 40 | for (let i = 0; i < types.length; i++) { 41 | types[i].records = await createType(types, i) 42 | } 43 | return types.reduce((acc, x) => { 44 | acc[x.id] = x.records 45 | return acc 46 | }, {}) 47 | } 48 | 49 | module.exports = { 50 | seedData 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lpwanserver-rest", 3 | "version": "1.2.1", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "nodemon app/index.js --watch app", 8 | "package": "node ./bin/package.js", 9 | "test:unit": "nyc --reporter=lcovonly mocha --opts test/unit/mocha.opts", 10 | "test:api": "mocha --opts test/api/mocha.opts", 11 | "test:e2e": "mocha --opts test/e2e/mocha.opts", 12 | "test:e2e-https": "mocha --opts test/e2e-https/mocha.opts", 13 | "clean": "./bin/clean.js", 14 | "prisma": "cd prisma && prisma", 15 | "lint": "eslint app/", 16 | "coverage": "cat ./coverage/lcov.info | codacy-coverage", 17 | "openapi:lint": "speccy lint docs/openapi/api.yml", 18 | "openapi:publish": "speccy resolve -o docs/openapi/dist/api.yml docs/openapi/api.yml && cp docs/openapi/dist/api.yml app/api.yml", 19 | "openapi:serve": "speccy serve docs/openapi/api.yml" 20 | }, 21 | "dependencies": { 22 | "@hapi/joi": "^15.1.1", 23 | "ajv": "^6.12.0", 24 | "async": "~2.6.1", 25 | "bluebird": "^3.7.2", 26 | "chai": "^4.2.0", 27 | "chai-http": "^4.2.0", 28 | "connect-ensure-login": "~0.1.1", 29 | "cookie-parser": "^1.4.5", 30 | "cors": "^2.8.5", 31 | "crypto": "~1.0.1", 32 | "db-migrate": "^0.11.4", 33 | "dead-leaves": "^1.0.2", 34 | "debug": "^4.1.0", 35 | "express": "^4.16.4", 36 | "express-busboy": "^7.0.1", 37 | "express-session": "^1.17.0", 38 | "jsmin": "~1.0.1", 39 | "jsonwebtoken": "^8.4.0", 40 | "mkdirp": "^0.5.3", 41 | "morgan": "^1.9.1", 42 | "nodemailer": "^6.4.16", 43 | "object-hash": "^1.3.1", 44 | "object-sizeof": "^1.5.3", 45 | "passport": "^0.4.1", 46 | "passport-local": "^1.0.0", 47 | "prisma-client-lib": "^1.34.10", 48 | "pug": "^3.0.1", 49 | "ramda": "^0.26.1", 50 | "redis": "^3.1.1", 51 | "request": "^2.88.2", 52 | "request-promise": "^4.2.5", 53 | "serve-favicon": "^2.5.0", 54 | "session-file-store": "^1.4.0", 55 | "ttn": "^2.3.3", 56 | "uuid": "^3.4.0", 57 | "winston": "^3.1.0", 58 | "ws": "^6.2.2" 59 | }, 60 | "devDependencies": { 61 | "axios": "^0.21.1", 62 | "codacy-coverage": "^3.4.0", 63 | "crypto-random-string": "^3.2.0", 64 | "eslint": "^6.8.0", 65 | "eslint-config-standard": "^12.0.0", 66 | "eslint-plugin-import": "^2.20.1", 67 | "eslint-plugin-node": "^8.0.0", 68 | "eslint-plugin-promise": "^4.0.1", 69 | "eslint-plugin-standard": "^4.0.1", 70 | "ip": "^1.1.5", 71 | "jsdoc": "^3.6.3", 72 | "mocha": "^6.2.2", 73 | "mocha-lcov-reporter": "^1.3.0", 74 | "nodemon": "^1.19.4", 75 | "nyc": "^14.1.1", 76 | "prisma": "^1.34.10", 77 | "speccy": "^0.11.0" 78 | }, 79 | "engines": { 80 | "node": ">=8.3.0", 81 | "npm": ">=5" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /prisma/lib/cache-instropection-query.js: -------------------------------------------------------------------------------- 1 | const { HttpLink } = require('apollo-link-http') 2 | const { introspectSchema } = require('graphql-tools') 3 | const fs = require('fs') 4 | const nodeFetch = require('node-fetch') 5 | const path = require('path') 6 | 7 | // Server fetches introspection query from prisma on startup 8 | // This fn caches the response so it can be read from a file 9 | // insead of http request for faster startup 10 | module.exports = async function cacheIntrospectionQuery (dirPath, uri) { 11 | const fetch = async (...args) => { 12 | const text = await nodeFetch(...args).then(x => x.text()) 13 | const writePath = path.join(dirPath, 'introspected-schema.json') 14 | fs.writeFileSync(writePath, text) 15 | return { text: () => Promise.resolve(text) } 16 | } 17 | return introspectSchema(new HttpLink({ uri, fetch })) 18 | } 19 | -------------------------------------------------------------------------------- /prisma/lib/post-deploy-tasks.js: -------------------------------------------------------------------------------- 1 | const cacheIntrospectionQuery = require('./cache-instropection-query') 2 | const path = require('path') 3 | 4 | const { env } = process 5 | 6 | const prismaUrl = `${env.prisma_protocol}://${env.prisma_host}:${env.prisma_port}` 7 | 8 | Promise.all([ 9 | cacheIntrospectionQuery(path.join(__dirname, '../generated'), prismaUrl) 10 | ]) 11 | .catch(e => console.error(e)) 12 | -------------------------------------------------------------------------------- /prisma/lib/seed-util.js: -------------------------------------------------------------------------------- 1 | const REL_PROPS = ['type', 'plural'] 2 | 3 | const lowerFirst = x => `${x[0].toLowerCase()}${x.slice(1)}` 4 | 5 | const connectId = seed => idPlaceholder => { 6 | const index = seed.bodyList.findIndex(x => x.id === idPlaceholder) 7 | console.log(JSON.stringify(seed.records[index])) 8 | return { id: seed.records[index].id } 9 | } 10 | 11 | const connectBody = (seeds, propMap = {}) => ({ id, ...body }) => { 12 | return Object.keys(body).reduce((acc, prop) => { 13 | const mappedProp = propMap[prop] || prop 14 | const relationship = seeds.find( 15 | seed => REL_PROPS.some(x => lowerFirst(seed[x]) === lowerFirst(mappedProp)) 16 | ) 17 | if (relationship) { 18 | const connect = connectId(relationship) 19 | if (Array.isArray(acc[prop])) { 20 | acc[prop] = { connect: acc[prop].map(connect) } 21 | } else { 22 | acc[prop] = { connect: connect(acc[prop]) } 23 | } 24 | } 25 | return acc 26 | }, body) 27 | } 28 | 29 | async function createRecordsOfType (type, seeds, prisma) { 30 | const seed = seeds.find(x => x.type === type) 31 | let bodyList = seed.bodyList.map(connectBody(seeds, seed.propMap)) 32 | const create = prisma[`create${type}`].bind(prisma) 33 | seed.records = await Promise.all(bodyList.map(create)) 34 | console.log(`Created ${seed.plural}:`, JSON.stringify(seed.records)) 35 | } 36 | 37 | module.exports = async function createRecords (seeds, prisma) { 38 | const types = seeds.map(x => x.type) 39 | for (let i = 0; i < types.length; i++) { 40 | await createRecordsOfType(types[i], seeds, prisma) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /prisma/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: ${env:prisma_url} 2 | datamodel: ./versions/v1/datamodel.prisma 3 | 4 | generate: 5 | - generator: javascript-client 6 | output: ../app/generated/prisma-client/ 7 | 8 | seed: 9 | run: node ./versions/v1/seed.js 10 | -------------------------------------------------------------------------------- /prisma/versions/v1/seed.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../../../app/generated/prisma-client') 2 | const createRecords = require('../../lib/seed-util') 3 | 4 | // example 5 | const seeds = [ 6 | { 7 | type: 'Company', 8 | plural: 'Companies', 9 | bodyList: [ 10 | { 11 | id: 'SYS_ADMINS', 12 | name: 'SysAdmins', 13 | type: 'ADMIN' 14 | } 15 | ] 16 | }, 17 | { 18 | type: 'NetworkType', 19 | plural: 'NetworkTypes', 20 | bodyList: [ 21 | { 22 | id: 'LORA', 23 | name: 'LoRa' 24 | }, 25 | { 26 | id: 'IP', 27 | name: 'IP' 28 | } 29 | ] 30 | }, 31 | { 32 | type: 'ReportingProtocol', 33 | plural: 'ReportingProtocols', 34 | bodyList: [ 35 | { 36 | id: 'POST', 37 | name: 'POST', 38 | protocolHandler: 'postHandler' 39 | } 40 | ] 41 | }, 42 | { 43 | type: 'User', 44 | plural: 'Users', 45 | bodyList: [ 46 | { 47 | id: 'ADMIN', 48 | username: 'admin', 49 | passwordHash: '000000100000003238bd33bdf92cfc3a8e7847e377e51ff8a3689913919b39d7dd0fe77c89610ce2947ab0b43a36895510d7d1f2924d84ab', 50 | email: 'fake@example.com', 51 | company: 'SYS_ADMINS', 52 | role: 'ADMIN' 53 | } 54 | ] 55 | }, 56 | { 57 | type: 'PasswordPolicy', 58 | plural: 'PasswordPolicies', 59 | bodyList: [ 60 | { 61 | id: 'PP1', 62 | ruleText: 'Must be at least 6 characters long', 63 | ruleRegExp: '^\\S{6,}$', 64 | company: 'SYS_ADMINS' 65 | } 66 | ] 67 | } 68 | ] 69 | 70 | createRecords(seeds, prisma).catch(e => console.error(e)) 71 | -------------------------------------------------------------------------------- /service_scripts/lpwanserver-rest.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=LPWANSERVERREST 3 | 4 | [Service] 5 | WorkingDirectory=/var/node/lpwanserver-rest 6 | ExecStart=/usr/bin/nodejs /var/node/lpwanserver-rest/app/index.js 7 | Restart=always 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /setup-ubuntu.sh: -------------------------------------------------------------------------------- 1 | cp service_scripts/* /etc/systemd/system/ 2 | npm install 3 | cd ui; npm install; cd .. 4 | systemctl start lpwanserver-rest 5 | systemctl start lpwanserver-ui 6 | systemctl enable lpwanserver-rest 7 | systemctl enable lpwanserver-ui 8 | -------------------------------------------------------------------------------- /test/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15 2 | 3 | WORKDIR /usr/src 4 | 5 | COPY package*.json ./ 6 | COPY app app 7 | COPY development/config.json config.json 8 | COPY test/api test/api 9 | COPY test/networks test/networks 10 | 11 | RUN npm install 12 | 13 | # Prevent server from attempting to serve UI 14 | ENV public_dir="" 15 | 16 | CMD ["npm", "run", "test:api"] 17 | -------------------------------------------------------------------------------- /test/api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | api-test: 5 | build: 6 | context: ../.. 7 | dockerfile: test/api/Dockerfile 8 | container_name: lpwanserver_dev_api_test 9 | networks: 10 | - lpwanserver_dev 11 | ports: 12 | - '3200:3200' 13 | environment: 14 | - config_file=../config.json 15 | networks: 16 | lpwanserver_dev: 17 | name: lpwanserver_dev 18 | -------------------------------------------------------------------------------- /test/api/mocha.opts: -------------------------------------------------------------------------------- 1 | # mocha.opts 2 | 3 | --recursive 4 | --reporter spec 5 | --bail 6 | --exit 7 | --spec test/api/tests/**/*.js 8 | --timeout 100000 9 | -------------------------------------------------------------------------------- /test/api/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm run clean 4 | 5 | # Start databases and prisma 6 | ./development/bin/manage-db start 7 | ./development/bin/manage-db deploy 8 | 9 | # Start ChirpStack 10 | docker-compose -f development/chirpstack/docker-compose.yml up -d 11 | 12 | # Run tests 13 | docker-compose -f test/api/docker-compose.yml up --build \ 14 | --abort-on-container-exit \ 15 | --exit-code-from api-test 16 | 17 | TEST_EXIT_CODE=$? 18 | 19 | #Stop ChirpStack 20 | docker-compose -f development/chirpstack/docker-compose.yml down 21 | 22 | # Stop databases 23 | ./development/bin/manage-db stop 24 | 25 | # Exit script with the code from the test 26 | exit $TEST_EXIT_CODE 27 | -------------------------------------------------------------------------------- /test/api/tests/001-restSessions.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var chai = require('chai') 3 | var chaiHttp = require('chai-http') 4 | const { createApp } = require('../../../app/express-app') 5 | var should = chai.should() 6 | 7 | chai.use(chaiHttp) 8 | var server 9 | 10 | describe('Sessions', () => { 11 | var adminToken 12 | before(async () => { 13 | const app = await createApp() 14 | server = chai.request(app).keepOpen() 15 | }) 16 | 17 | describe('POST /api/sessions', () => { 18 | it('should return 401 when the login is not valid', async () => { 19 | const res = await server 20 | .post('/api/sessions') 21 | .send({ 'login_username': 'foo', 'login_password': 'bar' }) 22 | res.should.have.status(401) 23 | }) 24 | 25 | it('should return 200 and a jwt when the login is valid', async () => { 26 | const res = await server 27 | .post('/api/sessions') 28 | .send({ 'login_username': 'admin', 'login_password': 'password' }) 29 | res.should.have.status(200) 30 | adminToken = res.text 31 | console.log("ADMIN_TOKEN", adminToken) 32 | }) 33 | 34 | it('should be able to use the admin jwt to get all companies', async () => { 35 | const res = await server 36 | .get('/api/companies') 37 | .set('Authorization', 'Bearer ' + adminToken) 38 | .set('Content-Type', 'application/json') 39 | res.should.have.status(200) 40 | var result = JSON.parse(res.text) 41 | result.records.should.be.instanceof(Array) 42 | }) 43 | }) 44 | 45 | describe('DELETE /api/sessions', () => { 46 | it('should return 401 with no token', async () => { 47 | const res = await server 48 | .delete('/api/sessions') 49 | res.should.have.status(401) 50 | }) 51 | 52 | it('should return 204 when the login is valid', async () => { 53 | const res = await server 54 | .delete('/api/sessions') 55 | .set('Authorization', 'Bearer ' + adminToken) 56 | res.should.have.status(204) 57 | adminToken = {} 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/data/index.js: -------------------------------------------------------------------------------- 1 | const R = require('ramda') 2 | 3 | const applicationTemplates = { 4 | default ({ name, companyId, networkTypeId, reportingProtocolId }) { 5 | const description = `${name} Description` 6 | const networkSettings = { name, description } 7 | return { 8 | app: { 9 | companyId, 10 | name, 11 | description, 12 | baseUrl: 'http://localhost:5086', 13 | reportingProtocolId 14 | }, 15 | networkSettings, 16 | appNTL: { 17 | 'applicationId': 0, 18 | networkTypeId, 19 | networkSettings 20 | } 21 | } 22 | } 23 | } 24 | 25 | const deviceTemplates = { 26 | weatherNode ({ name, companyId, devEUI, networkTypeId }) { 27 | return { 28 | deviceProfile: { 29 | networkTypeId, 30 | companyId, 31 | name: `LoRaWeatherNode_${name}`, 32 | description: 'GPS Node that works with LoRa', 33 | networkSettings: { 34 | name: `LoRaWeatherNode_${name}`, 35 | macVersion: '1.0.0', 36 | regParamsRevision: 'A', 37 | supportsJoin: true 38 | } 39 | }, 40 | device: { 41 | applicationId: '', 42 | name, 43 | description: 'GPS Node Model 001', 44 | deviceModel: 'Mark1' 45 | }, 46 | deviceNTL: { 47 | deviceId: '', 48 | networkTypeId, 49 | deviceProfileId: '', 50 | networkSettings: { 51 | devEUI, 52 | name, 53 | deviceKeys: { 54 | appKey: '11223344556677889900112233443311' 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | module.exports = { 63 | deviceTemplates, 64 | applicationTemplates 65 | } 66 | -------------------------------------------------------------------------------- /test/e2e-https/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15 2 | 3 | WORKDIR /usr/src 4 | 5 | COPY package*.json ./ 6 | COPY app app 7 | COPY development/config.json config.json 8 | COPY test/e2e-https test/e2e-https 9 | COPY test/networks test/networks 10 | COPY test/lib test/lib 11 | COPY lib lib 12 | COPY certs certs 13 | 14 | RUN npm install 15 | 16 | # Prevent server from attempting to serve UI 17 | ENV public_dir="" 18 | 19 | CMD ["npm", "run", "test:e2e-https"] 20 | -------------------------------------------------------------------------------- /test/e2e-https/clients/lora-server1.js: -------------------------------------------------------------------------------- 1 | const Client = require('../../../app/networkProtocols/handlers/LoraOpenSource/v1/client') 2 | 3 | const network = { 4 | id: 'mylora1servernetwork', 5 | baseUrl: `${process.env.LORA_APPSERVER1_URL}/api`, 6 | securityData: { 7 | username: 'admin', 8 | password: 'admin' 9 | } 10 | } 11 | 12 | const client = new Client() 13 | 14 | const handler = { 15 | get (obj, method) { 16 | return (...args) => obj[method](network, ...args) 17 | } 18 | } 19 | 20 | module.exports = { 21 | client: new Proxy(client, handler), 22 | cache: {}, 23 | network 24 | } 25 | -------------------------------------------------------------------------------- /test/e2e-https/clients/lora-server2.js: -------------------------------------------------------------------------------- 1 | const Client = require('../../../app/networkProtocols/handlers/LoraOpenSource/v2/client') 2 | 3 | const network = { 4 | id: 'mylora1servernetwork', 5 | baseUrl: `${process.env.LORA_APPSERVER2_URL}/api`, 6 | securityData: { 7 | username: 'admin', 8 | password: 'admin' 9 | } 10 | } 11 | 12 | const client = new Client() 13 | 14 | const handler = { 15 | get (obj, method) { 16 | return (...args) => obj[method](network, ...args) 17 | } 18 | } 19 | 20 | module.exports = { 21 | client: new Proxy(client, handler), 22 | cache: {}, 23 | network 24 | } 25 | -------------------------------------------------------------------------------- /test/e2e-https/clients/lpwan.js: -------------------------------------------------------------------------------- 1 | const { LpwanServerRestApi, LpwanServerRestApiCache } = require('../../lib/rest-client') 2 | const Axios = require('axios') 3 | const https = require('https') 4 | const path = require('path') 5 | const fs = require('fs') 6 | 7 | const caFile = path.join(__dirname, '../../../certs/ca-crt.pem') 8 | const ca = fs.readFileSync(caFile, { encoding: 'utf8' }) 9 | 10 | const catM1CertFile = path.join(__dirname, '../../../certs/client-catm1-crt.pem') 11 | const catM1Cert = fs.readFileSync(catM1CertFile, { encoding: 'utf8' }) 12 | const catM1KeyFile = path.join(__dirname, '../../../certs/client-catm1-key.pem') 13 | const catM1Key = fs.readFileSync(catM1KeyFile, { encoding: 'utf8' }) 14 | const catM1DeviceAgent = new https.Agent({ ca, key: catM1Key, cert: catM1Cert }) 15 | 16 | const nbIotCertFile = path.join(__dirname, '../../../certs/client-nbiot-crt.pem') 17 | const nbIotCert = fs.readFileSync(nbIotCertFile, { encoding: 'utf8' }) 18 | const nbIotKeyFile = path.join(__dirname, '../../../certs/client-nbiot-key.pem') 19 | const nbIotKey = fs.readFileSync(nbIotKeyFile, { encoding: 'utf8' }) 20 | const nbIotDeviceAgent = new https.Agent({ ca, key: nbIotKey, cert: nbIotCert }) 21 | 22 | function createLpwanClient () { 23 | const axios = Axios.create({ 24 | baseURL: `${process.env.LPWANSERVER_URL}/api`, 25 | httpsAgent: new https.Agent({ ca }) 26 | }) 27 | 28 | const cache = {} 29 | 30 | const client = new LpwanServerRestApi({ 31 | axios, 32 | cache: new LpwanServerRestApiCache({ cache }) 33 | }) 34 | 35 | return { client, cache } 36 | } 37 | 38 | module.exports = { 39 | createLpwanClient, 40 | catM1DeviceAgent, 41 | nbIotDeviceAgent 42 | } 43 | -------------------------------------------------------------------------------- /test/e2e-https/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | lpwanserver: 5 | build: 6 | context: ../.. 7 | dockerfile: Dockerfile 8 | image: lpwanserver/lpwanserver 9 | container_name: lpwanserver 10 | networks: 11 | - lpwanserver_dev 12 | ports: 13 | - '3200:3200' 14 | environment: 15 | - config_file=../config.json 16 | - public_dir= 17 | volumes: 18 | - ../../development/config.json:/usr/src/config.json 19 | - ../../certs:/usr/src/certs 20 | 21 | app-server: 22 | build: 23 | context: ../.. 24 | dockerfile: test/lib/rc-server/Dockerfile 25 | container_name: lpwanserver_dev_e2e_app_server 26 | networks: 27 | - lpwanserver_dev 28 | ports: 29 | - '3201:3201' 30 | environment: 31 | - PORT=3201 32 | 33 | e2e-test: 34 | build: 35 | context: ../.. 36 | dockerfile: test/e2e-https/Dockerfile 37 | container_name: lpwanserver_dev_e2e_test 38 | networks: 39 | - lpwanserver_dev 40 | depends_on: 41 | - lpwanserver 42 | - app-server 43 | environment: 44 | - config_file=../config.json 45 | - LPWANSERVER_URL=https://lpwanserver:3200 46 | - APP_SERVER_URL=http://app-server:3201 47 | - LORA_APPSERVER1_URL=https://chirpstack_app_svr_1:8080 48 | - LORA_APPSERVER2_URL=http://chirpstack_app_svr:8080 49 | - LORA_SERVER1_HOST_PORT=chirpstack_nwk_svr_1:8000 50 | - LORA_SERVER2_HOST_PORT=chirpstack_nwk_svr:8000 51 | - TTN_ENABLED=${TTN_ENABLED:-false} 52 | - TTN_USERNAME=${TTN_USERNAME} 53 | - TTN_PASSWORD=${TTN_PASSWORD} 54 | - TTN_CLIENT_ID=${TTN_CLIENT_ID} 55 | - TTN_CLIENT_SECRET=${TTN_CLIENT_SECRET} 56 | - LORIOT_ENABLED=${LORIOT_ENABLED:-false} 57 | - LORIOT_API_KEY=${LORIOT_API_KEY} 58 | volumes: 59 | - ../../certs:/usr/src/certs 60 | 61 | networks: 62 | lpwanserver_dev: 63 | name: lpwanserver_dev 64 | -------------------------------------------------------------------------------- /test/e2e-https/mocha.opts: -------------------------------------------------------------------------------- 1 | # mocha.opts 2 | 3 | --recursive 4 | --reporter spec 5 | --bail 6 | --exit 7 | --spec test/e2e-https/tests/**/*.spec.js 8 | --timeout 100000 9 | --file test/e2e-https/setup.js 10 | -------------------------------------------------------------------------------- /test/e2e-https/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm run clean 4 | 5 | # Start databases and prisma 6 | ./development/bin/manage-db start 7 | ./development/bin/manage-db deploy 8 | 9 | # Start ChirpStack 10 | docker-compose -f development/chirpstack/docker-compose.yml up -d 11 | 12 | # Run tests 13 | docker-compose -f test/e2e-https/docker-compose.yml up --build \ 14 | --abort-on-container-exit \ 15 | --exit-code-from e2e-test 16 | 17 | TEST_EXIT_CODE=$? 18 | 19 | #Stop ChirpStack 20 | docker-compose -f development/chirpstack/docker-compose.yml down 21 | 22 | # Stop databases 23 | ./development/bin/manage-db stop 24 | 25 | # Exit script with the code from the test 26 | exit $TEST_EXIT_CODE 27 | -------------------------------------------------------------------------------- /test/e2e-https/setup.js: -------------------------------------------------------------------------------- 1 | const Lora1 = require('./clients/lora-server1') 2 | const Lora2 = require('./clients/lora-server2') 3 | const Seeder = require('../../lib/seeder') 4 | const R = require('ramda') 5 | 6 | const commonSeeds = [ 7 | { 8 | id: 'NetworkServer', 9 | items: [ 10 | { 11 | id: 1, 12 | name: 'e2e_test_network_server' 13 | } 14 | ] 15 | }, 16 | { 17 | id: 'Organization', 18 | items: [ 19 | { 20 | id: 1, 21 | name: 'SysAdmins', 22 | displayName: 'SysAdmins', 23 | canHaveGateways: false 24 | } 25 | ] 26 | }, 27 | { 28 | id: 'ServiceProfile', 29 | items: [ 30 | { 31 | id: 1, 32 | name: 'e2e_test_service_profile', 33 | networkServerID: { $type: 'NetworkServer', $id: 1 }, 34 | organizationID: { $type: 'Organization', $id: 1 } 35 | } 36 | ] 37 | } 38 | ] 39 | 40 | async function seedLora1 () { 41 | let seeds = R.clone(commonSeeds) 42 | seeds[0].items[0].server = process.env.LORA_SERVER1_HOST_PORT 43 | seeds[0].create = async x => R.merge(x, await Lora1.client.createNetworkServer(x)) 44 | seeds[1].create = async x => R.merge(x, await Lora1.client.createOrganization(x)) 45 | seeds[2].create = async x => R.merge(x, await Lora1.client.createServiceProfile(x)) 46 | let result = await Seeder.seedData(seeds) 47 | Object.assign(Lora1.cache, result) 48 | return result 49 | } 50 | 51 | async function seedLora2 () { 52 | let seeds = R.clone(commonSeeds) 53 | seeds[0].items[0].server = process.env.LORA_SERVER2_HOST_PORT 54 | seeds[0].create = async x => R.merge(x, await Lora2.client.createNetworkServer(x)) 55 | seeds[1].create = async x => R.merge(x, await Lora2.client.createOrganization(x)) 56 | seeds[2].create = async x => R.merge(x, await Lora2.client.createServiceProfile(x)) 57 | let result = await Seeder.seedData(seeds) 58 | Object.assign(Lora2.cache, result) 59 | return result 60 | } 61 | 62 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) 63 | 64 | before(async () => { 65 | await wait(5000) // time needed for Lpwan Server to boot 66 | await Promise.all([ 67 | seedLora1(), 68 | seedLora2() 69 | ]) 70 | }) 71 | -------------------------------------------------------------------------------- /test/e2e-https/tests/device-import/setup.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../../../../app/generated/prisma-client') 2 | 3 | async function setupData () { 4 | const cos = await prisma.companies() 5 | let ipNwkType = await prisma.networkType({ name: 'IP' }) 6 | let postReportingProtocol = (await prisma.reportingProtocols())[0] 7 | let company = (await prisma.companies())[0] 8 | let deviceProfile = await prisma.createDeviceProfile({ 9 | company: { connect: { id: company.id } }, 10 | networkType: { connect: { id: ipNwkType.id } }, 11 | name: 'device-import-test-dev-prof', 12 | networkSettings: `{}` 13 | }) 14 | let application = await prisma.createApplication({ 15 | name: 'device-import-test-app', 16 | enabled: false, 17 | reportingProtocol: { connect: { id: postReportingProtocol.id } }, 18 | company: { connect: { id: cos[0].id } } 19 | }) 20 | 21 | return { 22 | ipNwkType, 23 | postReportingProtocol, 24 | company, 25 | deviceProfile, 26 | application 27 | } 28 | } 29 | 30 | module.exports = { 31 | setupData 32 | } 33 | -------------------------------------------------------------------------------- /test/e2e-https/tests/device-import/test.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { setupData } = require('./setup') 3 | const cryptoRandomString = require('crypto-random-string') 4 | const { createLpwanClient } = require('../../clients/lpwan') 5 | 6 | const { client: Lpwan } = createLpwanClient() 7 | 8 | describe('Bulk device import', () => { 9 | let Data 10 | 11 | before(async () => { 12 | Data = await setupData() 13 | await Lpwan.login({ 14 | data: { login_username: 'admin', login_password: 'password' } 15 | }) 16 | }) 17 | 18 | describe('Bulk upload IP devices', () => { 19 | it('Uploads a payload with a list of devices to import', async () => { 20 | const data = { 21 | deviceProfileId: Data.deviceProfile.id, 22 | devices: [ 23 | { devEUI: cryptoRandomString({ length: 16 }) }, 24 | { devEUI: cryptoRandomString({ length: 16 }) }, 25 | { devEUI: cryptoRandomString({ length: 16 }) } 26 | ] 27 | } 28 | const res = await Lpwan.importDevices({ id: Data.application.id }, { data }) 29 | assert.strictEqual(res.status, 200) 30 | }) 31 | 32 | it('Device import fails if no devEUI', async () => { 33 | const data = { 34 | deviceProfileId: Data.deviceProfile.id, 35 | devices: [ 36 | { devEUI: cryptoRandomString({ length: 16 }) }, 37 | { name: 'invalid' } 38 | ] 39 | } 40 | const res = await Lpwan.importDevices({ id: Data.application.id }, { data }) 41 | const invalid = res.data.filter(x => x.status === 'ERROR') 42 | assert.strictEqual(invalid.length, 1) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/e2e-https/tests/ip-device-messaging/setup.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../../../../app/generated/prisma-client') 2 | 3 | async function setupData ({ appBaseUrl }) { 4 | const cos = await prisma.companies() 5 | let ipNwkType = await prisma.networkType({ name: 'IP' }) 6 | let postReportingProtocol = (await prisma.reportingProtocols())[0] 7 | let company = (await prisma.companies())[0] 8 | let deviceProfile = await prisma.createDeviceProfile({ 9 | company: { connect: { id: company.id } }, 10 | networkType: { connect: { id: ipNwkType.id } }, 11 | name: 'ip-msg-test-dev-prof', 12 | networkSettings: `{}` 13 | }) 14 | let application = await prisma.createApplication({ 15 | name: 'ip-msg-test-app', 16 | baseUrl: appBaseUrl, 17 | enabled: true, 18 | reportingProtocol: { connect: { id: postReportingProtocol.id } }, 19 | company: { connect: { id: cos[0].id } } 20 | }) 21 | const device = await prisma.createDevice({ 22 | name: 'ip-msg-test-dvc', 23 | application: { connect: { id: application.id } } 24 | }) 25 | const deviceNtl = await prisma.createDeviceNetworkTypeLink({ 26 | networkType: { connect: { id: ipNwkType.id } }, 27 | device: { connect: { id: device.id } }, 28 | deviceProfile: { connect: { id: deviceProfile.id } }, 29 | networkSettings: '{"devEUI":"0011223344556677"}' 30 | }) 31 | 32 | return { 33 | ipNwkType, 34 | postReportingProtocol, 35 | company, 36 | deviceProfile, 37 | application, 38 | device, 39 | deviceNtl 40 | } 41 | } 42 | 43 | module.exports = { 44 | setupData 45 | } 46 | -------------------------------------------------------------------------------- /test/e2e-https/tests/ip-device-messaging/test.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { setupData } = require('./setup') 3 | const axios = require('axios') 4 | const { createLpwanClient, catM1DeviceAgent } = require('../../clients/lpwan') 5 | 6 | const { 7 | APP_SERVER_URL 8 | } = process.env 9 | 10 | const { client: Lpwan } = createLpwanClient() 11 | 12 | const uplinkPath = '/ip-device-messaging-uplinks' 13 | 14 | describe('E2E Test for IP Device Uplink/Downlink Device Messaging', () => { 15 | let Data 16 | 17 | before(async () => { 18 | Data = await setupData({ appBaseUrl: `${APP_SERVER_URL}${uplinkPath}` }) 19 | await Lpwan.login({ 20 | data: { login_username: 'admin', login_password: 'password' } 21 | }) 22 | }) 23 | 24 | describe('IP Device Uplink', () => { 25 | it('Send an uplink message to LPWAN Server IP uplink endpoint', async () => { 26 | const opts = { 27 | data: { msgId: 1 }, 28 | useSession: false, 29 | httpsAgent: catM1DeviceAgent 30 | } 31 | const res = await Lpwan.create('ip-device-uplinks', {}, opts) 32 | assert.strictEqual(res.status, 204) 33 | }) 34 | 35 | it('Ensure app server received message', async () => { 36 | const opts = { 37 | url: `${APP_SERVER_URL}/requests`, 38 | params: { 39 | method: 'POST', 40 | path: uplinkPath, 41 | body: JSON.stringify({ msgId: 1 }) 42 | } 43 | } 44 | const res = await axios(opts) 45 | assert.strictEqual(res.data.length, 1) 46 | }) 47 | }) 48 | 49 | describe('IP Device Downlink', () => { 50 | let downlink1 = { jsonData: { msgId: 2 }, fCnt: 0, fPort: 1 } 51 | 52 | it('Send a downlink request to LPWAN Server', async () => { 53 | await sendDownlink(Data.device.id, downlink1) 54 | }) 55 | 56 | it('Fetch downlink as IP device', async () => { 57 | const opts = { httpsAgent: catM1DeviceAgent } 58 | const res = await Lpwan.list('ip-device-downlinks', {}, opts) 59 | assert.deepStrictEqual(res.data[0], downlink1) 60 | }) 61 | 62 | it('Long poll for downlinks', async function () { 63 | this.timeout(10000) 64 | let downlink2 = { jsonData: { msgId: 3 }, fCnt: 0, fPort: 1 } 65 | 66 | setTimeout(() => sendDownlink(Data.device.id, downlink2), 3000) 67 | 68 | const opts = { 69 | httpsAgent: catM1DeviceAgent, 70 | headers: { prefer: 'wait=5' } 71 | } 72 | const res = await Lpwan.list('ip-device-downlinks', {}, opts) 73 | assert.deepStrictEqual(res.data[0], downlink2) 74 | }) 75 | }) 76 | }) 77 | 78 | async function sendDownlink (deviceId, data) { 79 | const res = await Lpwan.create('deviceDownlinks', { id: deviceId }, { data }) 80 | assert.strictEqual(res.status, 200) 81 | } 82 | -------------------------------------------------------------------------------- /test/e2e/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15 2 | 3 | WORKDIR /usr/src 4 | 5 | COPY package*.json ./ 6 | COPY app app 7 | COPY development/config.json config.json 8 | COPY test/e2e test/e2e 9 | COPY test/networks test/networks 10 | COPY test/lib test/lib 11 | COPY test/data test/data 12 | 13 | RUN npm install 14 | 15 | # Prevent server from attempting to serve UI 16 | ENV public_dir="" 17 | 18 | CMD ["npm", "run", "test:e2e"] 19 | -------------------------------------------------------------------------------- /test/e2e/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | e2e-test: 5 | build: 6 | context: ../.. 7 | dockerfile: test/e2e/Dockerfile 8 | container_name: lpwanserver_dev_e2e_test 9 | networks: 10 | - lpwanserver_dev 11 | ports: 12 | - 3200:3200 13 | environment: 14 | - config_file=../config.json 15 | - APP_SERVER_PORT=3201 16 | - TTN_ENABLED=${TTN_ENABLED:-false} 17 | - TTN_USERNAME=${TTN_USERNAME} 18 | - TTN_PASSWORD=${TTN_PASSWORD} 19 | - TTN_CLIENT_ID=${TTN_CLIENT_ID} 20 | - TTN_CLIENT_SECRET=${TTN_CLIENT_SECRET} 21 | - LORIOT_ENABLED=${LORIOT_ENABLED:-false} 22 | - LORIOT_API_KEY=${LORIOT_API_KEY} 23 | volumes: 24 | - ../../certs:/usr/src/certs 25 | networks: 26 | lpwanserver_dev: 27 | name: lpwanserver_dev 28 | -------------------------------------------------------------------------------- /test/e2e/mocha.opts: -------------------------------------------------------------------------------- 1 | # mocha.opts 2 | 3 | --recursive 4 | --reporter spec 5 | --bail 6 | --exit 7 | --spec test/e2e/tests/**/*.js 8 | --timeout 100000 9 | -------------------------------------------------------------------------------- /test/e2e/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm run clean 4 | 5 | # Start databases and prisma 6 | ./development/bin/manage-db start 7 | ./development/bin/manage-db deploy 8 | 9 | # Start ChirpStack 10 | docker-compose -f development/chirpstack/docker-compose.yml up -d 11 | 12 | # Run tests 13 | docker-compose -f test/e2e/docker-compose.yml up --build \ 14 | --abort-on-container-exit \ 15 | --exit-code-from e2e-test 16 | 17 | TEST_EXIT_CODE=$? 18 | 19 | #Stop ChirpStack 20 | docker-compose -f development/chirpstack/docker-compose.yml down 21 | 22 | # Stop databases 23 | ./development/bin/manage-db stop 24 | 25 | # Exit script with the code from the test 26 | exit $TEST_EXIT_CODE 27 | -------------------------------------------------------------------------------- /test/e2e/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | loraOSConfigured: false 3 | } 4 | module.exports.start = async function () { 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/tests/100-network-svr-cleanup.js: -------------------------------------------------------------------------------- 1 | const Loriot = require('../../networks/loriot') 2 | const Ttn = require('../../networks/ttn') 3 | 4 | const describeTTN = process.env.TTN_ENABLED === 'true' ? describe : describe.skip.bind(describe) 5 | const describeLoriot = process.env.LORIOT_ENABLED === 'true' ? describe : describe.skip.bind(describe) 6 | 7 | describe('Remove all data created on remote servers', () => { 8 | describeLoriot('Remove Loriot apps and devices', () => { 9 | it('Remove Loriot apps and devices', async () => { 10 | let { apps } = Loriot.client.listApplications(Loriot.network, { page: 1, perPage: 100 }) 11 | let appDevices = await Promise.all(apps.map(async app => { 12 | const appId = parseInt(app._id, 10) 13 | const { devices } = await Loriot.client.listDevices(Loriot.network, appId, { page: 1, perPage: 100 }) 14 | return { appId, devices } 15 | })) 16 | await Promise.all(appDevices.map(async ({ appId, devices }) => { 17 | await Promise.all(devices.map(dev => Loriot.client.deleteDevice(Loriot.network, appId, dev.id))) 18 | await Loriot.client.deleteApplication(Loriot.network, appId) 19 | })) 20 | }) 21 | }) 22 | describeTTN('Remove TTN apps and devices', () => { 23 | it('Remove TTN apps and devices', async () => { 24 | const deleteApp = async app => { 25 | try { 26 | const devices = await Ttn.client.listDevices(Ttn.network, app.id) 27 | await Promise.all(devices.map(dev => Ttn.client.deleteDevice(Ttn.network, app.id, dev.id))) 28 | await Ttn.client.unregisterApplication(Ttn.network, app.id) 29 | } 30 | catch (err) { 31 | } 32 | await Ttn.client.deleteAccountApplication(Ttn.network, app.id) 33 | } 34 | let apps = await Ttn.client.listApplications(Ttn.network) 35 | apps = apps.filter(x => x.id !== 'lpwansvr-e2e-app-hmoxiloq') 36 | await Promise.all(apps.map(deleteApp)) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/lib/helpers.js: -------------------------------------------------------------------------------- 1 | const R = require('ramda') 2 | 3 | const assertEqualProps = R.curry((props, obj1, obj2) => { 4 | const pickProps = R.pick(props) 5 | pickProps(obj1).should.deep.equal(pickProps(obj2)) 6 | }) 7 | 8 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) 9 | 10 | module.exports = { 11 | assertEqualProps, 12 | wait 13 | } 14 | -------------------------------------------------------------------------------- /test/lib/rc-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15-alpine 2 | 3 | # set working directory 4 | WORKDIR /usr/src/app 5 | 6 | # Copy project files 7 | COPY test/lib/rc-server/* ./ 8 | 9 | RUN npm install --production 10 | 11 | EXPOSE 8080 12 | 13 | CMD [ "node", "index.js" ] 14 | -------------------------------------------------------------------------------- /test/lib/rc-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-server", 3 | "version": "1.0.0", 4 | "description": "Remote Control Server - Command and Query with HTTP", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Nicholas Baroni", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "^4.17.1", 13 | "ramda": "^0.26.1", 14 | "request": "^2.88.0", 15 | "uuid": "^3.3.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/lib/rest-client/README.md: -------------------------------------------------------------------------------- 1 | # REST API Client 2 | The REST client is in development. 3 | 4 | ## Current Functionality 5 | The REST client can be used to call CRUD methods on all resources. 6 | It can also optionally manage the cache of the fetched records. 7 | 8 | ## Usage 9 | The REST client is used in the e2e-https tests. 10 | See `test/clients/lpwan.js` for how to setup. See test specs 11 | for how to make method calls. 12 | -------------------------------------------------------------------------------- /test/lib/rest-client/index.js: -------------------------------------------------------------------------------- 1 | const { AxiosRestApi, AxiosRestApiCache } = require('../axios-rest-client') 2 | 3 | class LpwanServerRestApi extends AxiosRestApi { 4 | constructor (opts) { 5 | super(opts) 6 | this.urls.deviceDownlinks = '/devices/:id/downlinks' 7 | this.urls.uplinks = '/ingest/:applicationId/:networkId' 8 | } 9 | 10 | async login (opts) { 11 | opts = { ...opts, url: '/sessions', method: 'POST' } 12 | const result = await this.axios(opts) 13 | this.authHeaders = { authorization: `Bearer ${result.data}` } 14 | return result.data 15 | } 16 | 17 | importDevices (urlParams, opts) { 18 | const url = `/applications/:id/import-devices` 19 | return this.axios(this._axiosOpts('applications', url, urlParams, { ...opts, method: 'POST' })) 20 | } 21 | } 22 | 23 | class LpwanServerRestApiCache extends AxiosRestApiCache { 24 | list (name, result) { 25 | const list = name === 'ip-device-downlinks' ? result.data : result.data.records 26 | return this._cacheUpsert(name, list) 27 | } 28 | } 29 | 30 | module.exports = { 31 | LpwanServerRestApi, 32 | LpwanServerRestApiCache 33 | } 34 | -------------------------------------------------------------------------------- /test/networks/lora-v1.js: -------------------------------------------------------------------------------- 1 | const Client = require('../../app/networkProtocols/handlers/LoraOpenSource/v1/client') 2 | const client = new Client() 3 | 4 | module.exports = { 5 | client, 6 | network: { 7 | id: '1', 8 | baseUrl: 'https://chirpstack_app_svr_1:8080/api', 9 | securityData: { 10 | username: 'admin', 11 | password: 'admin' 12 | } 13 | }, 14 | networkServer: { 15 | name: 'LoraOS1', 16 | server: 'chirpstack_nwk_svr_1:8000' 17 | }, 18 | organization: { 19 | name: 'SysAdmins', 20 | displayName: 'SysAdmins', 21 | canHaveGateways: false 22 | }, 23 | serviceProfile: { 24 | name: 'defaultForLPWANServer' 25 | }, 26 | deviceProfile: { 27 | name: 'BobMouseTrapDeviceProfileLv1', 28 | macVersion: '1.0.0' 29 | }, 30 | application: { 31 | name: 'BobMouseTrapLv1', 32 | description: 'CableLabs Test Application' 33 | }, 34 | device: { 35 | name: 'BobMouseTrapDeviceLv1', 36 | description: 'Test Device for E2E', 37 | devEUI: '3456789012345678' 38 | }, 39 | deviceActivation: { 40 | 'appSKey': '4ccbeee6e8b1852fe70fa01a5a16403e', 41 | 'devAddr': '01dd4aa3', 42 | 'devEUI': '3456789012345678', 43 | 'fCntDown': 0, 44 | 'fCntUp': 0, 45 | 'nwkSKey': 'b1f69424addc9e51c6da32d901fdc3f1', 46 | 'skipFCntCheck': true 47 | }, 48 | _setNetworkServerId (id) { 49 | this.networkServer.id = id 50 | this.serviceProfile.networkServerID = id 51 | this.deviceProfile.networkServerID = id 52 | }, 53 | _setOrgId (id) { 54 | this.organization.id = id 55 | this.serviceProfile.organizationID = id 56 | this.deviceProfile.organizationID = id 57 | this.application.organizationID = id 58 | }, 59 | _setServiceProfileId (id) { 60 | this.serviceProfile.id = id 61 | this.application.serviceProfileID = id 62 | }, 63 | _setDeviceProfileId (id) { 64 | this.deviceProfile.id = id 65 | this.device.deviceProfileID = id 66 | }, 67 | _setApplicationId (id) { 68 | this.application.id = id 69 | this.device.applicationID = id 70 | }, 71 | async setup () { 72 | let res 73 | // Create Network Server 74 | res = await client.createNetworkServer(this.network, this.networkServer) 75 | this._setNetworkServerId(res.id) 76 | // Create Org 77 | res = await client.createOrganization(this.network, this.organization) 78 | this._setOrgId(res.id) 79 | // Create Service Profile 80 | res = await client.createServiceProfile(this.network, this.serviceProfile) 81 | this._setServiceProfileId(res.id) 82 | // Create Device Profile 83 | res = await client.createDeviceProfile(this.network, this.deviceProfile) 84 | this._setDeviceProfileId(res.id) 85 | // Create App 86 | res = await client.createApplication(this.network, this.application) 87 | this._setApplicationId(res.id) 88 | // Create Device 89 | await client.createDevice(this.network, this.device) 90 | // Activate Device 91 | await client.activateDevice(this.network, this.device.devEUI, this.deviceActivation) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/networks/lora-v2.js: -------------------------------------------------------------------------------- 1 | const Client = require('../../app/networkProtocols/handlers/LoraOpenSource/v2/client') 2 | const client = new Client() 3 | 4 | module.exports = { 5 | client, 6 | network: { 7 | id: '2', 8 | baseUrl: 'http://chirpstack_app_svr:8080/api', 9 | securityData: { 10 | email: 'admin', 11 | password: 'admin' 12 | } 13 | }, 14 | networkServer: { 15 | name: 'LoraOS2', 16 | server: 'chirpstack_nwk_svr:8000' 17 | }, 18 | organization: { 19 | name: 'SysAdmins', 20 | displayName: 'SysAdmins', 21 | canHaveGateways: false 22 | }, 23 | serviceProfile: { 24 | name: 'defaultForLPWANServer' 25 | }, 26 | deviceProfile: { 27 | name: 'BobMouseTrapDeviceProfileLv2', 28 | macVersion: '1.0.0' 29 | }, 30 | application: { 31 | name: 'BobMouseTrapLv2', 32 | description: 'CableLabs Test Application' 33 | }, 34 | device: { 35 | name: 'BobMouseTrapDeviceLv2', 36 | description: 'Test Device for E2E', 37 | devEUI: '3344556677889900' 38 | }, 39 | deviceActivation: { 40 | 'aFCntDown': 0, 41 | 'appSKey': '290535e48e5e767d9810903db0e50298', 42 | 'devAddr': 'f9bdf0ab', 43 | 'devEUI': '3344556677889900', 44 | 'fCntUp': 0, 45 | 'fNwkSIntKey': '0e0f3622c801729c1dd2d644bd477244', 46 | 'nFCntDown': 0, 47 | 'nwkSEncKey': '0e0f3622c801729c1dd2d644bd477244', 48 | 'sNwkSIntKey': '0e0f3622c801729c1dd2d644bd477244' 49 | }, 50 | _setNetworkServerId (id) { 51 | this.networkServer.id = id 52 | this.serviceProfile.networkServerID = id 53 | this.deviceProfile.networkServerID = id 54 | }, 55 | _setOrgId (id) { 56 | this.organization.id = id 57 | this.serviceProfile.organizationID = id 58 | this.deviceProfile.organizationID = id 59 | this.application.organizationID = id 60 | }, 61 | _setServiceProfileId (id) { 62 | this.serviceProfile.id = id 63 | this.application.serviceProfileID = id 64 | }, 65 | _setDeviceProfileId (id) { 66 | this.deviceProfile.id = id 67 | this.device.deviceProfileID = id 68 | }, 69 | _setApplicationId (id) { 70 | this.application.id = id 71 | this.device.applicationID = id 72 | }, 73 | async setup () { 74 | let res 75 | // Create Network Server 76 | res = await client.createNetworkServer(this.network, this.networkServer) 77 | this._setNetworkServerId(res.id) 78 | // Create Org 79 | res = await client.createOrganization(this.network, this.organization) 80 | this._setOrgId(res.id) 81 | // Create Service Profile 82 | res = await client.createServiceProfile(this.network, this.serviceProfile) 83 | this._setServiceProfileId(res.id) 84 | // Create Device Profile 85 | res = await client.createDeviceProfile(this.network, this.deviceProfile) 86 | this._setDeviceProfileId(res.id) 87 | // Create App 88 | res = await client.createApplication(this.network, this.application) 89 | this._setApplicationId(res.id) 90 | // Create Device 91 | await client.createDevice(this.network, this.device) 92 | // Activate Device 93 | await client.activateDevice(this.network, this.device.devEUI, this.deviceActivation) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/networks/loriot.js: -------------------------------------------------------------------------------- 1 | const Client = require('../../app/networkProtocols/handlers/Loriot/v4/client') 2 | const client = new Client() 3 | 4 | module.exports = { 5 | client, 6 | network: { 7 | id: '3', 8 | baseUrl: 'https://us1.loriot.io/1/nwk', 9 | securityData: { 10 | apiKey: process.env.LORIOT_API_KEY 11 | } 12 | }, 13 | application: { 14 | title: 'ApiTest', 15 | capacity: 10 16 | }, 17 | device: { 18 | title: 'ApiTestDevice', 19 | description: 'Test Device for E2E', 20 | deveui: '0080000004001546', 21 | devclass: 'A', 22 | lorawan: { major: '1', minor: '0', revision: '0' } 23 | }, 24 | _setApplicationId (id) { 25 | this.application.id = id 26 | }, 27 | _setDeviceId (id) { 28 | this.device.id = id 29 | }, 30 | async setup () { 31 | let res 32 | // Create App 33 | res = await client.createApplication(this.network, this.application) 34 | this._setApplicationId(parseInt(res._id, 10)) 35 | // Create Device 36 | // Loriot automatically assigns the device ABP properties, so no need to activate 37 | res = await client.createDevice(this.network, this.application.id, this.device) 38 | this._setDeviceId(res._id) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/networks/ttn.js: -------------------------------------------------------------------------------- 1 | const Client = require('../../app/networkProtocols/handlers/TheThingsNetwork/TtnRestClient') 2 | const client = new Client() 3 | const { key } = require('ttn') 4 | // const crypto = require('crypto') 5 | 6 | // const randomString = crypto.randomBytes(6).toString('base64').toLowerCase() 7 | const APP_ID = `lpwansvr-e2e-app-hmoxiloq` 8 | 9 | module.exports = { 10 | client, 11 | network: { 12 | id: '4', 13 | baseUrl: 'https://account.thethingsnetwork.org', 14 | securityData: { 15 | username: process.env.TTN_USERNAME, 16 | password: process.env.TTN_PASSWORD, 17 | clientId: process.env.TTN_CLIENT_ID, 18 | clientSecret: process.env.TTN_CLIENT_SECRET 19 | } 20 | }, 21 | application: { 22 | id: APP_ID, 23 | name: 'CableLabs Prototype', 24 | description: 'App for LPWAN Server E2E tests', 25 | rights: [ 26 | 'settings', 27 | 'delete', 28 | 'collaborators', 29 | 'devices' 30 | ], 31 | access_keys: [ 32 | { 33 | 'rights': [ 34 | 'settings', 35 | 'devices', 36 | 'messages:up:r', 37 | 'messages:down:w' 38 | ], 39 | 'name': 'lpwan' 40 | } 41 | ] 42 | }, 43 | abpDevice: { 44 | altitude: 0, 45 | app_id: APP_ID, 46 | description: 'CableLabs TTN Device ABP', 47 | dev_id: `lpwansvr-e2e-abp-hmoxiloq`, 48 | latitude: 52.375, 49 | longitude: 4.887, 50 | lorawan_device: { 51 | dev_eui: '006476D29AA36C97', 52 | dev_id: `lpwansvr-e2e-abp-hmoxiloq`, 53 | app_id: APP_ID, 54 | activation_constraints: 'abp', 55 | app_s_key: key(16), 56 | nwk_s_key: key(16), 57 | dev_addr: '01dd4aa4', 58 | f_cnt_down: 0, 59 | f_cnt_up: 0, 60 | disable_f_cnt_check: false 61 | } 62 | }, 63 | otaaDevice: { 64 | altitude: 0, 65 | appID: APP_ID, 66 | description: 'TTN Device Using OTAA', 67 | dev_id: `lpwansvr-e2e-otaa-hmoxiloq`, 68 | latitude: 52.375, 69 | longitude: 4.887, 70 | lorawan_device: { 71 | dev_eui: '003C48905977AD1C', 72 | dev_id: `lpwansvr-e2e-otaa-hmoxiloq`, 73 | app_id: APP_ID, 74 | activation_constraints: 'otaa', 75 | app_key: key(16) 76 | } 77 | }, 78 | async setup () { 79 | // Create App 80 | await client.createApplication(this.network, this.application) 81 | await client.registerApplication(this.network, this.application.id) 82 | // Create Devices 83 | await client.createDevice(this.network, this.application.id, this.abpDevice) 84 | await client.createDevice(this.network, this.application.id, this.otaaDevice) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/unit/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15 2 | 3 | WORKDIR /usr/src 4 | 5 | COPY package*.json ./ 6 | COPY app app 7 | COPY development/config.json config.json 8 | COPY test/unit test/unit 9 | 10 | RUN npm install 11 | 12 | # Prevent server from attempting to serve UI 13 | ENV public_dir="" 14 | 15 | CMD ["npm", "run", "test:unit"] 16 | -------------------------------------------------------------------------------- /test/unit/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | unit-test: 5 | build: 6 | context: ../.. 7 | dockerfile: test/unit/Dockerfile 8 | container_name: lpwanerver_unit_test 9 | networks: 10 | - lpwanserver_dev 11 | ports: 12 | - '3200:3200' 13 | environment: 14 | - config_file=../config.json 15 | volumes: 16 | - ../../coverage:/usr/src/coverage 17 | networks: 18 | lpwanserver_dev: 19 | name: lpwanserver_dev 20 | volumes: 21 | coverage: 22 | name: lpwanserver_test_coverage 23 | -------------------------------------------------------------------------------- /test/unit/mocha.opts: -------------------------------------------------------------------------------- 1 | # mocha.opts 2 | 3 | --recursive 4 | --bail 5 | --exit 6 | --spec test/unit/tests/**/*.js 7 | --timeout 100000 8 | --file test/unit/setup.js 9 | -------------------------------------------------------------------------------- /test/unit/mock/ModelAPI-mock.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | dataValue: null, 4 | networks: { 5 | async load (networkId) { 6 | return { 7 | networkId: 1, 8 | networkProtocolId: 1 9 | } 10 | } 11 | }, 12 | networkProtocols: { 13 | async load (networkProtocolId) { 14 | return ({}) 15 | } 16 | }, 17 | networkProtocolAPI: { 18 | async getProtocol (network) { 19 | return { 20 | sessionData: {}, 21 | api: require('../../../rest/networkProtocols/LoRaOpenSource_2.js') 22 | } 23 | }, 24 | async connect () { 25 | }, 26 | async test () { 27 | } 28 | }, 29 | networkTypeAPI: { 30 | async addDeviceProfile (nwkId, dpId) { 31 | return ({}) 32 | }, 33 | async pushDeviceProfile (nwkId, dpId) { 34 | return ({}) 35 | }, 36 | async connect () { 37 | return ({}) 38 | }, 39 | async test () { 40 | return ({}) 41 | } 42 | }, 43 | protocolData: { 44 | async load () { 45 | return { dataValue: this.dataValue } 46 | }, 47 | async create (networkId, networkProtocolId, key, data) { 48 | this.dataValue = data 49 | }, 50 | async loadValue () { 51 | return this.dataValue 52 | }, 53 | async upsert (network, key, data) { 54 | this.dataValue = data 55 | } 56 | }, 57 | reportingProtocols: { 58 | async load (id) { 59 | if (id === 1) { 60 | return ( 61 | { 62 | id: id, 63 | reportingProtocolId: 1, 64 | protocolHandler: 'postHandler.js' 65 | }) 66 | } 67 | else { 68 | throw new Error('Application Not Found') 69 | } 70 | }, 71 | getHandler () { 72 | return { report: () => ({}) } 73 | } 74 | }, 75 | passwordPolicies: { 76 | validatePassword () { 77 | return true 78 | } 79 | }, 80 | applicationNetworkTypeLinks: { 81 | async list () { 82 | return [[]] 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/unit/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Remove old containers, volumes, and images 4 | npm run clean 5 | 6 | # Start databases and prisma 7 | ./development/bin/manage-db start 8 | ./development/bin/manage-db deploy 9 | 10 | # Run docker-compose with development configuration 11 | docker-compose \ 12 | -f ./test/unit/docker-compose.yml up --build \ 13 | --abort-on-container-exit \ 14 | --exit-code-from unit-test 15 | 16 | TEST_EXIT_CODE=$? 17 | 18 | # Stop databases 19 | ./development/bin/manage-db stop 20 | 21 | # Exit script with the code from the test 22 | exit $TEST_EXIT_CODE 23 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | const { redisClient, redisPub, redisSub } = require('../../app/lib/redis') 2 | 3 | console.log(process.env.prisma_url) 4 | 5 | after(() => Promise.all([ 6 | redisClient.quit(), 7 | redisPub.quit(), 8 | redisSub.quit() 9 | ])) 10 | -------------------------------------------------------------------------------- /test/unit/tests/lib/crypto-test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | var assert = require('assert') 3 | // eslint-disable-next-line no-unused-vars 4 | var chai = require('chai') 5 | // eslint-disable-next-line no-unused-vars 6 | var should = chai.should() 7 | var TestModule = require('../../../../app/lib/crypto') 8 | const testName = 'Crypto' 9 | 10 | describe('Unit Tests for ' + testName, () => { 11 | let temp = null 12 | before('Setup ENV', async () => { 13 | }) 14 | after('Shutdown', async () => { 15 | }) 16 | it(testName + ' Hash Password', async () => { 17 | temp = await TestModule.hashPassword('Test123') 18 | }) 19 | it(testName + ' Verify Password', async () => { 20 | const valid = await TestModule.verifyPassword('Test123', temp) 21 | if (valid) return 22 | throw new Error('Password Failed') 23 | }) 24 | it(testName + ' Fail to Verify Password', async () => { 25 | const valid = await TestModule.verifyPassword('Test456', temp) 26 | if (!valid) return 27 | throw new Error('Password Did not fail') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/unit/tests/models/IDevice-test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const assert = require('assert') 3 | // eslint-disable-next-line no-unused-vars 4 | const chai = require('chai') 5 | // eslint-disable-next-line no-unused-vars 6 | const should = chai.should() 7 | const { prisma } = require('../../../../app/generated/prisma-client') 8 | const TestModule = require('../../../../app/models/IDevice') 9 | const modelAPIMock = require('../../mock/ModelAPI-mock') 10 | const testName = 'Device' 11 | 12 | function assertDeviceProps (actual) { 13 | actual.should.have.property('name') 14 | actual.should.have.property('description') 15 | actual.application.should.have.property('id') 16 | actual.should.have.property('deviceModel') 17 | actual.should.have.property('id') 18 | } 19 | 20 | describe('Unit Tests for ' + testName, () => { 21 | let deviceId = '' 22 | before('Setup ENV', async () => {}) 23 | it(testName + ' Construction', () => { 24 | let testModule = new TestModule(modelAPIMock) 25 | should.exist(testModule) 26 | }) 27 | it(testName + ' Empty Retrieval', async () => { 28 | let testModule = new TestModule(modelAPIMock) 29 | should.exist(testModule) 30 | const actual = await testModule.list() 31 | actual.should.have.length(2) 32 | }) 33 | it(testName + ' Create', async () => { 34 | const apps = await prisma.applications() 35 | let testModule = new TestModule(modelAPIMock) 36 | should.exist(testModule) 37 | const devData = { 38 | name: 'test', 39 | description: 'test application', 40 | applicationId: apps[0].id, 41 | deviceModel: 'AR1' 42 | } 43 | const actual = await testModule.create(devData) 44 | assertDeviceProps(actual) 45 | deviceId = actual.id 46 | }) 47 | it(testName + ' Retrieve', async () => { 48 | let testModule = new TestModule(modelAPIMock) 49 | should.exist(testModule) 50 | const actual = await testModule.load(deviceId) 51 | assertDeviceProps(actual) 52 | }) 53 | it(testName + ' Update', async () => { 54 | let testModule = new TestModule(modelAPIMock) 55 | should.exist(testModule) 56 | let updated = { 57 | id: deviceId, 58 | name: 'test', 59 | description: 'updated description', 60 | deviceModel: 'AR2' 61 | } 62 | const actual = await testModule.update(updated) 63 | assertDeviceProps(actual) 64 | actual.description.should.equal(updated.description) 65 | actual.deviceModel.should.equal(updated.deviceModel) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/unit/tests/models/IDeviceProfile-test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const assert = require('assert') 3 | // eslint-disable-next-line no-unused-vars 4 | const chai = require('chai') 5 | // eslint-disable-next-line no-unused-vars 6 | const should = chai.should() 7 | const { prisma } = require('../../../../app/generated/prisma-client') 8 | const TestModule = require('../../../../app/models/IDeviceProfile') 9 | const testName = 'DeviceProfile' 10 | const modelAPIMock = require('../../mock/ModelAPI-mock') 11 | 12 | function assertDeviceProfileProps (actual) { 13 | actual.should.have.property('name') 14 | actual.should.have.property('description') 15 | actual.networkType.should.have.property('id') 16 | actual.company.should.have.property('id') 17 | actual.should.have.property('id') 18 | } 19 | 20 | describe('Unit Tests for ' + testName, () => { 21 | let deviceProfileId = '' 22 | before('Setup ENV', async () => {}) 23 | after('Shutdown', async () => {}) 24 | it(testName + ' Construction', () => { 25 | let testModule = new TestModule(modelAPIMock) 26 | should.exist(testModule) 27 | }) 28 | it(testName + ' Empty Retrieval', async () => { 29 | let testModule = new TestModule(modelAPIMock) 30 | should.exist(testModule) 31 | const actual = await testModule.list() 32 | actual.should.have.length(2) 33 | }) 34 | it(testName + ' Create', async () => { 35 | const nwkTypes = await prisma.networkTypes() 36 | const cos = await prisma.companies() 37 | let testModule = new TestModule(modelAPIMock) 38 | should.exist(testModule) 39 | const actual = await testModule.create(nwkTypes[0].id, cos[0].id, 'test', 'test description') 40 | assertDeviceProfileProps(actual) 41 | deviceProfileId = actual.id 42 | }) 43 | it(testName + ' Retrieve', async () => { 44 | let testModule = new TestModule(modelAPIMock) 45 | should.exist(testModule) 46 | const actual = await testModule.load(deviceProfileId) 47 | assertDeviceProfileProps(actual) 48 | }) 49 | it(testName + ' Update', async () => { 50 | let testModule = new TestModule(modelAPIMock) 51 | should.exist(testModule) 52 | let updated = { 53 | id: deviceProfileId, 54 | name: 'test', 55 | description: 'updated description' 56 | } 57 | const actual = await testModule.update(updated) 58 | assertDeviceProfileProps(actual) 59 | actual.description.should.equal(updated.description) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/unit/tests/models/IUser-test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const assert = require('assert') 3 | // eslint-disable-next-line no-unused-vars 4 | const chai = require('chai') 5 | // eslint-disable-next-line no-unused-vars 6 | const should = chai.should() 7 | const { prisma } = require('../../../../app/generated/prisma-client') 8 | const TestModule = require('../../../../app/models/IUser') 9 | const modelAPIMock = require('../../mock/ModelAPI-mock') 10 | 11 | const testName = 'User' 12 | 13 | function assertUserProps (actual) { 14 | actual.should.have.property('username') 15 | actual.should.have.property('email') 16 | actual.should.have.property('role') 17 | actual.company.should.have.property('id') 18 | actual.should.have.property('id') 19 | } 20 | 21 | describe('Unit Tests for ' + testName, () => { 22 | let userId = '' 23 | before('Setup ENV', async () => {}) 24 | after('Shutdown', async () => {}) 25 | it(testName + ' Construction', () => { 26 | let testModule = new TestModule(modelAPIMock) 27 | should.exist(testModule) 28 | }) 29 | it(testName + ' Empty Retrieval', async () => { 30 | let testModule = new TestModule(modelAPIMock) 31 | should.exist(testModule) 32 | const actual = await testModule.list() 33 | actual.should.have.length(2) 34 | }) 35 | it(testName + ' Create', async () => { 36 | const cos = await prisma.companies() 37 | let testModule = new TestModule(modelAPIMock) 38 | should.exist(testModule) 39 | const data = { 40 | username: 'testuser', 41 | password: '123456', 42 | email: 'bob@aol.com', 43 | companyId: cos[0].id, 44 | role: 'ADMIN' 45 | } 46 | const actual = await testModule.create(data) 47 | assertUserProps(actual) 48 | userId = actual.id 49 | }) 50 | it(testName + ' Retrieve', async () => { 51 | let testModule = new TestModule(modelAPIMock) 52 | should.exist(testModule) 53 | const actual = await testModule.load(userId) 54 | assertUserProps(actual) 55 | }) 56 | it(testName + ' Update', async () => { 57 | let testModule = new TestModule(modelAPIMock) 58 | should.exist(testModule) 59 | let updated = { 60 | id: userId, 61 | username: 'testuser', 62 | email: 'bob@aol.com', 63 | role: 'USER' 64 | } 65 | const actual = await testModule.update(updated) 66 | assertUserProps(actual) 67 | actual.role.should.equal(updated.role) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/unit/tests/reportingProtocols/postHandler-test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | var assert = require('assert') 3 | // eslint-disable-next-line no-unused-vars 4 | var chai = require('chai') 5 | // eslint-disable-next-line no-unused-vars 6 | var should = chai.should() 7 | var TestModule = require('../../../../app/reportingProtocols/postHandler') 8 | const testName = 'Post Handler' 9 | const { promisify } = require('util') 10 | 11 | // content of index.js 12 | const http = require('http') 13 | const port = 5000 14 | 15 | const requestHandler = (request, response) => { 16 | console.log(request.url) 17 | response.statusCode = 201 18 | response.end(JSON.stringify({ message: 'Hello Client' })) 19 | } 20 | 21 | const server = http.createServer(requestHandler) 22 | const serverListen = promisify(server.listen.bind(server)) 23 | const serverClose = promisify(server.close.bind(server)) 24 | 25 | describe('Unit Tests for ' + testName, () => { 26 | before('Setup ENV', async () => { 27 | await serverListen(port) 28 | }) 29 | after('Shutdown', async () => { 30 | await serverClose() 31 | }) 32 | it(testName + ' Report', async () => { 33 | let dataObject = { name: 'Hello App' } 34 | const actual = await (new TestModule()).report(dataObject, 'http://localhost:5000', 'test') 35 | actual.statusCode.should.equal(201) 36 | actual.body.message.should.equal('Hello Client') 37 | }) 38 | it(testName + ' Report Empty', async () => { 39 | const actual = await (new TestModule()).report(null, 'http://localhost:5000', 'test') 40 | actual.statusCode.should.equal(201) 41 | actual.body.message.should.equal('Hello Client') 42 | }) 43 | it(testName + ' Report No App', async () => { 44 | let dataObject = { name: 'Hello App' } 45 | const actual = await (new TestModule()).report(dataObject, 'http://localhost:5000') 46 | actual.statusCode.should.equal(201) 47 | actual.body.message.should.equal('Hello Client') 48 | }) 49 | it(testName + ' Report Incorrect URL', async () => { 50 | let dataObject = { name: 'Hello App' } 51 | try { 52 | await (new TestModule()).report(dataObject, 'http://localhost:6000', 'test') 53 | throw new Error('Should not connect to incorrect url') 54 | } 55 | catch (err) { 56 | err.message.should.equal('connect ECONNREFUSED 127.0.0.1:6000') 57 | } 58 | }) 59 | }) 60 | --------------------------------------------------------------------------------