Merge pull request #19 from Permissionless-Software-Foundation/ct-unstable

Heavy refactoring and relocation of directories.
This commit is contained in:
Chris Troutner
2021-08-08 11:19:24 -07:00
committed by GitHub
16 changed files with 1200 additions and 743 deletions
+1
View File
@@ -64,6 +64,7 @@ database/
system-user-*.json
orbitdb
ipfsdata
.ipfsdata
ipfs-service-provider.sh
!README.md
+62 -51
View File
@@ -1,3 +1,12 @@
/*
This Koa server has two interfaces:
- REST API over HTTP
- JSON RPC over IPFS
The architecture of the code follows the Clean Architecture pattern:
https://troutsblog.com/blog/clean-architecture
*/
// npm libraries
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
@@ -12,73 +21,75 @@ const cors = require('kcors')
// Local libraries
const config = require('../config') // this first.
// const IPFSLib = require('../src/lib/ipfs')
const AdminLib = require('../src/adapters/admin')
const adminLib = new AdminLib()
// const JSONRPC = require('../src/rpc')
// const rpc = new JSONRPC()
const errorMiddleware = require('../src/controllers/rest-api/middleware/error')
const { wlogger } = require('../src/adapters/wlogger')
async function startServer () {
// Create a Koa instance.
const app = new Koa()
app.keys = [config.session]
class Server {
constructor () {
this.adminLib = new AdminLib()
}
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(config.database, {
useUnifiedTopology: true,
useNewUrlParser: true
})
async startServer () {
try {
// Create a Koa instance.
const app = new Koa()
app.keys = [config.session]
// MIDDLEWARE START
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(config.database, {
useUnifiedTopology: true,
useNewUrlParser: true
})
app.use(convert(logger()))
app.use(bodyParser())
app.use(session())
app.use(errorMiddleware())
// MIDDLEWARE START
// Used to generate the docs.
app.use(mount('/', serve(`${process.cwd()}/docs`)))
app.use(convert(logger()))
app.use(bodyParser())
app.use(session())
app.use(errorMiddleware())
// Mount the page for displaying logs.
app.use(mount('/logs', serve(`${process.cwd()}/config/logs`)))
// Used to generate the docs.
app.use(mount('/', serve(`${process.cwd()}/docs`)))
// User Authentication
require('../config/passport')
app.use(passport.initialize())
app.use(passport.session())
// Mount the page for displaying logs.
app.use(mount('/logs', serve(`${process.cwd()}/config/logs`)))
// Attach REST API and JSON RPC controllers to the app.
const Controllers = require('../src/controllers')
const controllers = new Controllers()
controllers.attachControllers(app)
// User Authentication
require('../config/passport')
app.use(passport.initialize())
app.use(passport.session())
// Enable CORS for testing
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
app.use(cors({ origin: '*' }))
// Attach REST API and JSON RPC controllers to the app.
const Controllers = require('../src/controllers')
const controllers = new Controllers()
await controllers.attachControllers(app)
// MIDDLEWARE END
app.controllers = controllers
console.log(`Running server in environment: ${config.env}`)
wlogger.info(`Running server in environment: ${config.env}`)
// Enable CORS for testing
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
app.use(cors({ origin: '*' }))
await app.listen(config.port)
console.log(`Server started on ${config.port}`)
// MIDDLEWARE END
// Create the system admin user.
const success = await adminLib.createSystemUser()
if (success) console.log('System admin user created.')
console.log(`Running server in environment: ${config.env}`)
wlogger.info(`Running server in environment: ${config.env}`)
return app
await app.listen(config.port)
console.log(`Server started on ${config.port}`)
// Create the system admin user.
const success = await this.adminLib.createSystemUser()
if (success) console.log('System admin user created.')
return app
} catch (err) {
console.error('Could not start server. Error: ', err)
}
}
}
// startServer()
// export default app
// module.exports = app
module.exports = {
startServer
}
module.exports = Server
+15
View File
@@ -31,6 +31,21 @@ module.exports = {
? process.env.EMAILPASS
: 'emailpassword',
// FullStack.cash account information, used for automatic JWT handling.
getJwtAtStartup: false,
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',
fullstackPassword: process.env.FULLSTACKPASS
? process.env.FULLSTACKPASS
: 'demo',
// IPFS settings.
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
-1
View File
@@ -1 +0,0 @@
This is where the mongodb docker container stores its db files.
+2 -1
View File
@@ -1,3 +1,4 @@
const server = require('./bin/server.js')
const Server = require('./bin/server.js')
const server = new Server()
server.startServer()
+782 -645
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -23,14 +23,15 @@
},
"repository": "Permissionless-Software-Foundation/ipfs-service-provider",
"dependencies": {
"@psf/bch-js": "^4.20.1",
"@psf/bch-js": "^4.20.3",
"axios": "^0.21.1",
"bcryptjs": "^2.4.3",
"glob": "^7.1.6",
"ipfs": "^0.54.4",
"ipfs-coord": "^3.2.0",
"ipfs": "0.54.4",
"ipfs-coord": "^5.0.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",
+107
View File
@@ -0,0 +1,107 @@
/*
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.fullstackLogin
if (!this.login || typeof this.login !== 'string') {
throw new Error(
'Must pass a FullStack.cash login (email) instantiating FullStackJWT class.'
)
}
this.password = localConfig.fullstackPassword
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.authServer,
login: this.login,
password: this.password
})
// State
this.apiToken = '' // Default value.
this.bchjs = {}
}
// Get's a JWT token from FullStack.cash.
async getJWT () {
try {
// Skip connecting FullStack.cash auth server to the network if this is an E2E test.
if (process.env.TEST_TYPE === 'e2e') {
this.apiToken = 'faketoken'
return this.apiToken
}
// 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 () {
this.bchjs = new BCHJS({
restURL: this.apiServer,
apiToken: this.apiToken
})
return this.bchjs
}
}
module.exports = FullStackJWT
+41 -16
View File
@@ -4,29 +4,54 @@
https://troutsblog.com/blog/clean-architecture
*/
// Public NPM libraries
const BCHJS = require('@psf/bch-js')
// Load individual adapter libraries.
const IPFSAdapter = require('./ipfs')
const LocalDB = require('./localdb')
const LogsAPI = require('./logapi')
const Passport = require('./passport')
const Nodemailer = require('./nodemailer')
const { wlogger } = require('./wlogger')
// const { wlogger } = require('./wlogger')
const JSONFiles = require('./json-files')
const FullStackJWT = require('./fullstack-jwt')
// Instantiate adapter libraries.
const ipfs = new IPFSAdapter()
const localdb = new LocalDB()
const logapi = new LogsAPI()
const passport = new Passport()
const nodemailer = new Nodemailer()
const jsonFiles = new JSONFiles()
const config = require('../../config')
module.exports = {
ipfs,
localdb,
logapi,
passport,
nodemailer,
wlogger,
jsonFiles
class Adapters {
constructor (localConfig = {}) {
// Encapsulate dependencies
this.ipfs = new IPFSAdapter()
this.localdb = new LocalDB()
this.logapi = new LogsAPI()
this.passport = new Passport()
this.nodemailer = new Nodemailer()
this.jsonFiles = new JSONFiles()
this.bchjs = new BCHJS()
this.config = config
// Get a valid JWT API key and instance bch-js.
this.fullStackJwt = new FullStackJWT(config)
}
async start () {
try {
if (this.config.getJwtAtStartup) {
// 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 this.fullStackJwt.getJWT()
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
this.bchjs = await this.fullStackJwt.instanceBchjs()
}
// Start the IPFS node.
await this.ipfs.start()
} catch (err) {
console.error('Error in adapters/index.js/start()')
throw err
}
}
}
module.exports = Adapters
+1 -1
View File
@@ -50,7 +50,7 @@ class IpfsCoordAdapter {
})
// Wait for the ipfs-coord library to signal that it is ready.
await this.ipfsCoord.isReady()
await this.ipfsCoord.start()
// Signal that this adapter is ready.
this.isReady = true
+5 -11
View File
@@ -25,7 +25,7 @@ class IpfsAdapter {
try {
// Ipfs Options
const ipfsOptions = {
repo: './ipfsdata',
repo: './.ipfsdata/ipfs',
start: true,
config: {
relay: {
@@ -56,16 +56,6 @@ class IpfsAdapter {
// Set the 'server' profile so the node does not scan private networks.
await this.ipfs.config.profiles.apply('server')
// const nodeConfig = await this.ipfs.config.getAll()
// console.log(
// `IPFS node configuration: ${JSON.stringify(nodeConfig, null, 2)}`
// )
// Stop the IPFS node if we're running tests.
if (this.config.env === 'test') {
await this.ipfs.stop()
}
// Signal that this adapter is ready.
this.isReady = true
@@ -75,6 +65,10 @@ class IpfsAdapter {
throw err
}
}
async stop () {
await this.ipfs.stop()
}
}
module.exports = IpfsAdapter
+8 -8
View File
@@ -7,7 +7,7 @@
// Public npm libraries.
// Load the Clean Architecture Adapters library
const adapters = require('../adapters')
const Adapters = require('../adapters')
// Load the JSON RPC Controller.
const JSONRPC = require('./json-rpc')
@@ -21,30 +21,30 @@ const RESTControllers = require('./rest-api')
class Controllers {
constructor (localConfig = {}) {
this.adapters = adapters
this.useCases = new UseCases({ adapters })
this.adapters = new Adapters()
this.useCases = new UseCases({ adapters: this.adapters })
}
async attachControllers (app) {
// Wait for any startup processes to complete for the Adapters libraries.
await this.adapters.start()
// Attach the REST controllers to the Koa app.
this.attachRESTControllers(app)
// Start IPFS.
await this.adapters.ipfs.start()
this.attachRPCControllers()
}
// Top-level function for this library.
// Start the various Controllers and attach them to the app.
attachRESTControllers (app) {
const rESTControllers = new RESTControllers({
const restControllers = new RESTControllers({
adapters: this.adapters,
useCases: this.useCases
})
// Attach the REST API Controllers associated with the boilerplate code to the Koa app.
rESTControllers.attachRESTControllers(app)
restControllers.attachRESTControllers(app)
}
// Add the JSON RPC router to the ipfs-coord adapter.
+6 -1
View File
@@ -10,7 +10,7 @@ const axios = require('axios').default
// Local support libraries
const config = require('../../../config')
const app = require('../../../bin/server')
const Server = require('../../../bin/server')
const testUtils = require('../../utils/test-utils')
const AdminLib = require('../../../src/adapters/admin')
const adminLib = new AdminLib()
@@ -22,9 +22,14 @@ const LOCALHOST = `http://localhost:${config.port}`
describe('Auth', () => {
before(async () => {
const app = new Server()
// This should be the first instruction. It starts the REST API server.
await app.startServer()
// Stop the IPFS node for the rest of the e2e tests.
// await app.controllers.adapters.ipfs.stop()
// Delete all previous users in the database.
await testUtils.deleteAllUsers()
+2 -1
View File
@@ -12,7 +12,8 @@ const LOCALHOST = `http://localhost:${config.port}`
const context = {}
const UserController = require('../../../src/controllers/rest-api/users/controller')
const adapters = require('../../../src/adapters')
const Adapters = require('../../../src/adapters')
const adapters = new Adapters()
const UseCases = require('../../../src/use-cases/')
let uut
let sandbox
@@ -0,0 +1,162 @@
/*
Unit tests for the jwt-bch-lib and fullstack-jwt.js adapter library.
*/
const assert = require('chai').assert
const sinon = require('sinon')
const FullStackJWT = require('../../../src/adapters/fullstack-jwt')
describe('#FullStackJWT', () => {
let sandbox
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
const localConfig = {
authServer: 'someserver',
apiServer: 'someserver',
fullstackLogin: 'somelogin',
fullstackPassword: 'somepassword'
}
uut = new FullStackJWT(localConfig)
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if auth server is not specified', () => {
try {
uut = new FullStackJWT()
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a url for the AUTH server when instantiating FullStackJWT class.'
)
}
})
it('should throw an error if api server is not specified', () => {
try {
const localConfig = {
authServer: 'someserver'
}
uut = new FullStackJWT(localConfig)
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a url for the API server when instantiating FullStackJWT class.'
)
}
})
it('should throw an error if login is not specified', () => {
try {
const localConfig = {
authServer: 'someserver',
apiServer: 'someserver'
}
uut = new FullStackJWT(localConfig)
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a FullStack.cash login (email) instantiating FullStackJWT class.'
)
}
})
it('should throw an error if login is not specified', () => {
try {
const localConfig = {
authServer: 'someserver',
apiServer: 'someserver',
fullstackLogin: 'somelogin'
}
uut = new FullStackJWT(localConfig)
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a FullStack.cash account password when instantiating FullStackJWT class.'
)
}
})
})
describe('#getJWT', () => {
it('should return the JWT token', async () => {
// Mock dependencies to force a code path.
sandbox.stub(uut.jwtLib, 'register').resolves({})
uut.jwtLib.userData.apiToken = 'abc123'
sandbox.stub(uut.jwtLib, 'validateApiToken').resolves({ isValid: true })
const result = await uut.getJWT()
// console.log('result: ', result)
assert.equal(result, 'abc123')
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut.jwtLib, 'register').rejects(new Error('test error'))
await uut.getJWT()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should throw an error if user does not have a JWT', async () => {
try {
// Mock dependencies to force a code path.
sandbox.stub(uut.jwtLib, 'register').resolves({})
await uut.getJWT()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err.message: ', err.message)
assert.include(err.message, 'This account does not have a JWT')
}
})
it('should retrieve a new JWT token if the old one invalid', async () => {
// Mock dependencies to force a code path.
sandbox.stub(uut.jwtLib, 'register').resolves({})
uut.jwtLib.userData.apiToken = 'abc123'
uut.jwtLib.userData.apiLevel = 30
sandbox.stub(uut.jwtLib, 'validateApiToken').resolves({ isValid: false })
sandbox.stub(uut.jwtLib, 'getApiToken').resolves('xyz789')
const result = await uut.getJWT()
// console.log('result: ', result)
assert.equal(result, 'xyz789')
})
})
describe('#instanceBchjs', () => {
it('should return an instance of bch-js', () => {
const result = uut.instanceBchjs()
// console.log('result: ', result)
assert.property(result, 'restURL')
})
})
})
+2 -4
View File
@@ -6,8 +6,6 @@
// const assert = require('chai').assert
const sinon = require('sinon')
const adapters = require('../../../src/adapters')
// const { attachControllers } = require('../../../src/controllers')
const Controllers = require('../../../src/controllers')
describe('#Controllers', () => {
@@ -25,8 +23,8 @@ describe('#Controllers', () => {
describe('#attachControllers', () => {
it('should attach the controllers', async () => {
// mock IPFS
sandbox.stub(adapters.ipfs, 'start').resolves({})
adapters.ipfs.ipfsCoordAdapter = {
sandbox.stub(uut.adapters, 'start').resolves({})
uut.adapters.ipfs.ipfsCoordAdapter = {
attachRPCRouter: () => {}
}