From 126bee86bd0da3bdddefe1eed9adacb73e1134bb Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 16 Jul 2021 17:26:51 -0700 Subject: [PATCH] feat(JWT): Added automatic JWT handling for FullStack.cash --- bin/server.js | 10 +- config/env/common.js | 12 +++ package-lock.json | 17 ++++ package.json | 1 + src/adapters/fullstack-jwt.js | 106 +++++++++++++++++++++ src/adapters/index.js | 17 +++- src/adapters/ipfs/index.js | 12 ++- src/adapters/ipfs/ipfs-coord.js | 9 +- src/controllers/index.js | 21 +++- src/controllers/json-rpc/bch/index.js | 5 +- src/controllers/rest-api/bch/controller.js | 5 +- 11 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 src/adapters/fullstack-jwt.js diff --git a/bin/server.js b/bin/server.js index b826e5b..07bbd2d 100644 --- a/bin/server.js +++ b/bin/server.js @@ -69,8 +69,14 @@ async function startServer () { console.log(`Server started on ${config.port}`) // Create the system admin user. - const success = await adminLib.createSystemUser() - if (success) console.log('System admin user created.') + try { + const success = await adminLib.createSystemUser() + if (success) console.log('System admin user created.') + } catch (err) { + console.warn( + 'Error trying to create system admin. Perhaps one already exists?' + ) + } return app } diff --git a/config/env/common.js b/config/env/common.js index 5ac9089..8665297 100644 --- a/config/env/common.js +++ b/config/env/common.js @@ -23,6 +23,18 @@ module.exports = { ? process.env.EMAILPASS : 'emailpassword', + // FullStack.cash account information, used for automatic JWT handling. + AUTHSERVER: process.env.AUTHSERVER + ? process.env.AUTHSERVER + : 'https://auth.fullstack.cash', + APISERVER: process.env.APISERVER + ? process.env.APISERVER + : 'https://api.fullstack.cash/v5/', + FULLSTACKLOGIN: process.env.FULLSTACKLOGIN + ? process.env.FULLSTACKLOGIN + : 'demo@demo.com', + FULLSTACKPASS: process.env.FULLSTACKPASS ? process.env.FULLSTACKPASS : 'demo', + // IPFS settings. isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false, diff --git a/package-lock.json b/package-lock.json index 4c7e77c..485a6da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "ipfs-coord": "^3.2.0", "jsonrpc-lite": "^2.2.0", "jsonwebtoken": "^8.5.1", + "jwt-bch-lib": "^1.3.0", "kcors": "^2.2.2", "koa": "^2.13.1", "koa-bodyparser": "^4.3.0", @@ -13576,6 +13577,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwt-bch-lib": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jwt-bch-lib/-/jwt-bch-lib-1.3.0.tgz", + "integrity": "sha512-CZs3jUHJlWvConvoTeLDogDEwUysKeojYJxfUhbwJKJrtDCSfi3ZWbsZZ8WN30Xs28aZPDB+qlHPJlTXQmup0w==", + "dependencies": { + "axios": "^0.21.1" + } + }, "node_modules/k-bucket": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/k-bucket/-/k-bucket-5.1.0.tgz", @@ -37694,6 +37703,14 @@ "safe-buffer": "^5.0.1" } }, + "jwt-bch-lib": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jwt-bch-lib/-/jwt-bch-lib-1.3.0.tgz", + "integrity": "sha512-CZs3jUHJlWvConvoTeLDogDEwUysKeojYJxfUhbwJKJrtDCSfi3ZWbsZZ8WN30Xs28aZPDB+qlHPJlTXQmup0w==", + "requires": { + "axios": "^0.21.1" + } + }, "k-bucket": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/k-bucket/-/k-bucket-5.1.0.tgz", diff --git a/package.json b/package.json index 9ee2153..b5764fb 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "ipfs-coord": "^3.2.0", "jsonrpc-lite": "^2.2.0", "jsonwebtoken": "^8.5.1", + "jwt-bch-lib": "^1.3.0", "kcors": "^2.2.2", "koa": "^2.13.1", "koa-bodyparser": "^4.3.0", diff --git a/src/adapters/fullstack-jwt.js b/src/adapters/fullstack-jwt.js new file mode 100644 index 0000000..95ab65e --- /dev/null +++ b/src/adapters/fullstack-jwt.js @@ -0,0 +1,106 @@ +/* + A library of utility functions for working with FullStack.cash JWT tokens. + + Feel free to copy this library into your own app, as well as the unit tests + for this file. +*/ + +const JwtLib = require('jwt-bch-lib') +const BCHJS = require('@psf/bch-js') + +class FullStackJWT { + constructor (localConfig = {}) { + // Input Validation + this.authServer = localConfig.authServer + if (!this.authServer || typeof this.authServer !== 'string') { + throw new Error( + 'Must pass a url for the AUTH server when instantiating FullStackJWT class.' + ) + } + this.apiServer = localConfig.apiServer + if (!this.apiServer || typeof this.apiServer !== 'string') { + throw new Error( + 'Must pass a url for the API server when instantiating FullStackJWT class.' + ) + } + this.login = localConfig.login + if (!this.login || typeof this.login !== 'string') { + throw new Error( + 'Must pass a FullStack.cash login (email) instantiating FullStackJWT class.' + ) + } + this.password = localConfig.password + if (!this.password || typeof this.password !== 'string') { + throw new Error( + 'Must pass a FullStack.cash account password when instantiating FullStackJWT class.' + ) + } + + // Encapsulate dependencies + this.jwtLib = new JwtLib({ + // Overwrite default values with the values in the config file. + server: this.server, + login: this.login, + password: this.password + }) + + // State + this.apiToken = '' // Default value. + this.bchjs = {} + } + + // Get's a JWT token from FullStack.cash. + async getJWT () { + try { + // Log into the auth server. + await this.jwtLib.register() + + this.apiToken = this.jwtLib.userData.apiToken + if (!this.apiToken) { + throw new Error('This account does not have a JWT') + } + console.log(`Retrieved JWT token: ${this.apiToken}\n`) + + // Ensure the JWT token is valid to use. + const isValid = await this.jwtLib.validateApiToken() + + // Get a new token with the same API level, if the existing token is not + // valid (probably expired). + if (!isValid.isValid) { + this.apiToken = await this.jwtLib.getApiToken( + this.jwtLib.userData.apiLevel + ) + console.log( + `The JWT token was not valid. Retrieved new JWT token: ${this.apiToken}\n` + ) + } else { + console.log('JWT token is valid.\n') + } + + return this.apiToken + } catch (err) { + console.error( + `Error trying to log into ${this.server} and retrieve JWT token.` + ) + throw err + } + } + + // Create an instance of bchjs with the validated JWT token. Returns this + // instance of bch-js. + instanceBchjs () { + try { + this.bchjs = new BCHJS({ + restURL: this.apiServer, + apiToken: this.apiToken + }) + + return this.bchjs + } catch (err) { + console.error('Error in instanceBchjs()') + throw err + } + } +} + +module.exports = FullStackJWT diff --git a/src/adapters/index.js b/src/adapters/index.js index 846e89f..c9591b5 100644 --- a/src/adapters/index.js +++ b/src/adapters/index.js @@ -4,6 +4,9 @@ https://troutsblog.com/blog/clean-architecture */ +// Load the config file +const config = require('../../config') + // Load individual adapter libraries. const IPFSAdapter = require('./ipfs') const LocalDB = require('./localdb') @@ -12,6 +15,7 @@ const Passport = require('./passport') const Nodemailer = require('./nodemailer') const { wlogger } = require('./wlogger') const JSONFiles = require('./json-files') +const FullStackJWT = require('./fullstack-jwt') // Instantiate adapter libraries. const ipfs = new IPFSAdapter() @@ -21,6 +25,15 @@ const passport = new Passport() const nodemailer = new Nodemailer() const jsonFiles = new JSONFiles() +// Get a valid JWT API key and instance bch-js. +const fullStackJwt = new FullStackJWT({ + authServer: config.AUTHSERVER, + apiServer: config.APISERVER, + login: config.FULLSTACKLOGIN, + password: config.FULLSTACKPASS +}) +const bchjs = {} // Placeholder. + module.exports = { ipfs, localdb, @@ -28,5 +41,7 @@ module.exports = { passport, nodemailer, wlogger, - jsonFiles + jsonFiles, + fullStackJwt, + bchjs } diff --git a/src/adapters/ipfs/index.js b/src/adapters/ipfs/index.js index 1fd7fb8..acc75d2 100644 --- a/src/adapters/ipfs/index.js +++ b/src/adapters/ipfs/index.js @@ -19,8 +19,15 @@ class IPFS { // Provides a global start() function that triggers the start() function in // the underlying libraries. - async start () { + async start (localConfig = {}) { try { + const bchjs = localConfig.bchjs + if (!bchjs) { + throw new Error( + 'Instance of bch-js must be passed when instantiating IPFS adapter.' + ) + } + // Start IPFS await this.ipfsAdapter.start() console.log('IPFS is ready.') @@ -30,7 +37,8 @@ class IPFS { // Start ipfs-coord this.ipfsCoordAdapter = new this.IpfsCoordAdapter({ - ipfs: this.ipfs + ipfs: this.ipfs, + bchjs }) await this.ipfsCoordAdapter.start() console.log('ipfs-coord is ready.') diff --git a/src/adapters/ipfs/ipfs-coord.js b/src/adapters/ipfs/ipfs-coord.js index 7406afd..ff9b9e2 100644 --- a/src/adapters/ipfs/ipfs-coord.js +++ b/src/adapters/ipfs/ipfs-coord.js @@ -6,7 +6,7 @@ // Global npm libraries const IpfsCoord = require('ipfs-coord') -const BCHJS = require('@psf/bch-js') +// const BCHJS = require('@psf/bch-js') // Local libraries const config = require('../../../config') @@ -23,11 +23,16 @@ class IpfsCoordAdapter { 'Instance of IPFS must be passed when instantiating ipfs-coord.' ) } + this.bchjs = localConfig.bchjs + if (!this.bchjs) { + throw new Error( + 'Instance of bch-js must be passed when instantiating ipfs-coord.' + ) + } // Encapsulate dependencies this.IpfsCoord = IpfsCoord this.ipfsCoord = {} - this.bchjs = new BCHJS() // this.rpc = new JSONRPC() this.config = config diff --git a/src/controllers/index.js b/src/controllers/index.js index 1692740..b8d1f1c 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -22,13 +22,24 @@ const RESTControllers = require('./rest-api') // Top-level function for this library. // Start the various Controllers and attach them to the app. async function attachControllers (app) { - // Attach the REST controllers to the Koa app. - attachRESTControllers(app) + try { + // Get a JWT token and instantiate bch-js with it. Then pass that instance + // to all the rest of the apps controllers and adapters. + await adapters.fullStackJwt.getJWT() + // Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js. + adapters.bchjs = await adapters.fullStackJwt.instanceBchjs() - // Start IPFS. - await adapters.ipfs.start() + // Attach the REST controllers to the Koa app. + attachRESTControllers(app) - attachRPCControllers() + // Start IPFS. + await adapters.ipfs.start({ bchjs: adapters.bchjs }) + + attachRPCControllers() + } catch (err) { + console.error('Error in attachControllers()') + throw err + } } function attachRESTControllers (app) { diff --git a/src/controllers/json-rpc/bch/index.js b/src/controllers/json-rpc/bch/index.js index 0191db1..2959c55 100644 --- a/src/controllers/json-rpc/bch/index.js +++ b/src/controllers/json-rpc/bch/index.js @@ -4,7 +4,7 @@ // Public npm libraries const jsonrpc = require('jsonrpc-lite') -const BCHJS = require('@psf/bch-js') +// const BCHJS = require('@psf/bch-js') // Local libraries // const UserLib = require('../../../use-cases/user') @@ -32,7 +32,8 @@ class BCHRPC { this.jsonrpc = jsonrpc this.validators = new Validators(localConfig) this.rateLimit = new RateLimit() - this.bchjs = new BCHJS() + // this.bchjs = new BCHJS() + this.bchjs = this.adapters.bchjs } // Top-level router for this library. All other methods in this class are for diff --git a/src/controllers/rest-api/bch/controller.js b/src/controllers/rest-api/bch/controller.js index 3ebc264..2ea8d7b 100644 --- a/src/controllers/rest-api/bch/controller.js +++ b/src/controllers/rest-api/bch/controller.js @@ -2,7 +2,7 @@ Controller for the /fulcrum REST API endpoints. */ -const BCHJS = require('@psf/bch-js') +// const BCHJS = require('@psf/bch-js') let _this @@ -22,7 +22,8 @@ class BCHRESTController { ) } - this.bchjs = new BCHJS() + // this.bchjs = new BCHJS() + this.bchjs = this.adapters.bchjs _this = this }