mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-21 16:52:00 -07:00
Forked from ipfs-swap-service
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
# A special property that should be specified at the top of the file outside of
|
||||
# any sections. Set to true to stop .editor config file search on current file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# Indentation style
|
||||
# Possible values - tab, space
|
||||
indent_style = space
|
||||
|
||||
# Indentation size in single-spaced characters
|
||||
# Possible values - an integer, tab
|
||||
indent_size = 2
|
||||
|
||||
# Line ending file format
|
||||
# Possible values - lf, crlf, cr
|
||||
end_of_line = lf
|
||||
|
||||
# File character encoding
|
||||
# Possible values - latin1, utf-8, utf-16be, utf-16le
|
||||
charset = utf-8
|
||||
|
||||
# Denotes whether to trim whitespace at the end of lines
|
||||
# Possible values - true, false
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Denotes whether file should end with a newline
|
||||
# Possible values - true, false
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
|
||||
|
||||
# Created by https://www.gitignore.io/api/node,sublimetext
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
#logs
|
||||
logs/*.json
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
|
||||
### SublimeText ###
|
||||
# cache files for sublime text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using SublimeText
|
||||
*.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
#Documentation
|
||||
docs
|
||||
|
||||
.nyc_output
|
||||
coverage
|
||||
database/
|
||||
system-user-*.json
|
||||
orbitdb
|
||||
ipfsdata
|
||||
.ipfsdata
|
||||
ipfs-service-provider.sh
|
||||
run-dev.sh
|
||||
wallet.json
|
||||
|
||||
!README.md
|
||||
@@ -0,0 +1,8 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2021 Permissionless Software Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,33 @@
|
||||
# avax-dex
|
||||
|
||||
[](http://standardjs.com) [](https://github.com/semantic-release/semantic-release)
|
||||
|
||||
This is a prototype web service that monitors the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) for trading signals, to trade AVAX and Avalanche Native Tokens (ANTs) on the AVAX X-Chain. It's inspired by the [SWaP Prototype](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md).
|
||||
|
||||
## Development Notes
|
||||
|
||||
**Warning**: This repository is under active development. Things will be constantly changing and breaking.
|
||||
|
||||
### Road Map:
|
||||
|
||||
- Phase 1: MVP (single user)
|
||||
- Move tokens or AVAX for sale to temporary address.
|
||||
- Accept input from user:
|
||||
- Submit Signal message to P2WDB for new buy/sell order.
|
||||
- Check UTXO status before submitting Payment message.
|
||||
- React to webhook when new Offers come in.
|
||||
- Add and track new Offers in MongoDB.
|
||||
- Process Payment signals:
|
||||
- Checking transaction for compatibility
|
||||
- Broadcast/close trades managed by this app.
|
||||
- Phase 2: Build single-user web app
|
||||
- _Make_ buy or sell orders.
|
||||
- Browse Offers on the market.
|
||||
- _Take_ buy or sell orders.
|
||||
- Phase 3: Multi-user
|
||||
- Configure REST API as an IPFS Service that can offer services to multiple users.
|
||||
- Update web app to display and select available SWaP services over IPFS.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE.md)
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sampleUrl": null
|
||||
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
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')
|
||||
const convert = require('koa-convert')
|
||||
const logger = require('koa-logger')
|
||||
const mongoose = require('mongoose')
|
||||
const session = require('koa-generic-session')
|
||||
const passport = require('koa-passport')
|
||||
const mount = require('koa-mount')
|
||||
const serve = require('koa-static')
|
||||
const cors = require('kcors')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../config') // this first.
|
||||
const AdminLib = require('../src/adapters/admin')
|
||||
// const adminLib = new AdminLib()
|
||||
|
||||
const WebHookLib = require('../src/adapters/webhook')
|
||||
const webhookLib = new WebHookLib()
|
||||
|
||||
// const JSONRPC = require('../src/rpc')
|
||||
// const rpc = new JSONRPC()
|
||||
|
||||
const errorMiddleware = require('../src/controllers/rest-api/middleware/error')
|
||||
// const { wlogger } = require('../src/adapters/wlogger')
|
||||
|
||||
class Server {
|
||||
constructor () {
|
||||
this.adminLib = new AdminLib()
|
||||
}
|
||||
|
||||
async startServer () {
|
||||
try {
|
||||
// Create a Koa instance.
|
||||
const app = new Koa()
|
||||
app.keys = [config.session]
|
||||
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
console.log(
|
||||
`Connecting to MongoDB with this connection string: ${config.database}`
|
||||
)
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
console.log(`Starting environment: ${config.env}`)
|
||||
console.log(`Debug level: ${config.debugLevel}`)
|
||||
|
||||
// MIDDLEWARE START
|
||||
|
||||
app.use(convert(logger()))
|
||||
app.use(bodyParser())
|
||||
app.use(session())
|
||||
app.use(errorMiddleware())
|
||||
|
||||
// Used to generate the docs.
|
||||
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
||||
|
||||
// Mount the page for displaying logs.
|
||||
app.use(mount('/logs', serve(`${process.cwd()}/config/logs`)))
|
||||
|
||||
// User Authentication
|
||||
require('../config/passport')
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
|
||||
// Attach REST API and JSON RPC controllers to the app.
|
||||
const Controllers = require('../src/controllers')
|
||||
const controllers = new Controllers()
|
||||
await controllers.attachRESTControllers(app)
|
||||
|
||||
app.controllers = controllers
|
||||
|
||||
// Enable CORS for testing
|
||||
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
|
||||
app.use(cors({ origin: '*' }))
|
||||
|
||||
// MIDDLEWARE END
|
||||
|
||||
// Create webhook
|
||||
try {
|
||||
try {
|
||||
// Delete an old webhook if it exists.
|
||||
await webhookLib.deleteWebhook('http://localhost:5700/order')
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
// console.log('err deleting webhook: ', err)
|
||||
}
|
||||
|
||||
await webhookLib.createWebhook('http://localhost:5700/order')
|
||||
console.log('Webhook created')
|
||||
} catch (error) {
|
||||
console.log('Webhook cant be created')
|
||||
}
|
||||
|
||||
// startServer()
|
||||
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.')
|
||||
|
||||
// Attach the other IPFS controllers
|
||||
await controllers.attachControllers(app)
|
||||
|
||||
// ipfs-coord has a memory leak. This app shuts down after 4 hours. It
|
||||
// expects to be run by Docker or pm2, which can automatically restart
|
||||
// the app.
|
||||
setTimeout(function () {
|
||||
process.exit(0)
|
||||
}, 60000 * 60 * 2) // 2 hours
|
||||
|
||||
return app
|
||||
} catch (err) {
|
||||
console.error('Could not start server. Error: ', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Server
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
This file is used to store unsecure, application-specific data common to all
|
||||
environments.
|
||||
*/
|
||||
|
||||
/* eslint no-unneeded-ternary:0 */
|
||||
|
||||
// Get the version from the package.json file.
|
||||
const pkgInfo = require('../../package.json')
|
||||
const version = pkgInfo.version
|
||||
|
||||
const ipfsCoordName = process.env.COORD_NAME
|
||||
? process.env.COORD_NAME
|
||||
: 'ipfs-torlist-service-generic'
|
||||
|
||||
module.exports = {
|
||||
// Configure TCP port.
|
||||
port: process.env.PORT || 5002,
|
||||
|
||||
// Password for HTML UI that displays logs.
|
||||
logPass: 'test',
|
||||
|
||||
// Email server settings if nodemailer email notifications are used.
|
||||
emailServer: process.env.EMAILSERVER
|
||||
? process.env.EMAILSERVER
|
||||
: 'mail.someserver.com',
|
||||
emailUser: process.env.EMAILUSER
|
||||
? process.env.EMAILUSER
|
||||
: 'noreply@someserver.com',
|
||||
emailPassword: process.env.EMAILPASS
|
||||
? process.env.EMAILPASS
|
||||
: 'emailpassword',
|
||||
|
||||
// FullStack.cash account information, used for automatic JWT handling.
|
||||
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : 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,
|
||||
// SSL domain used for websocket connection via browsers.
|
||||
crDomain: process.env.CR_DOMAIN ? process.env.CR_DOMAIN : '',
|
||||
|
||||
// Information passed to other IPFS peers about this node.
|
||||
apiInfo: 'https://ipfs-service-provider.fullstack.cash/',
|
||||
|
||||
// JSON-LD and Schema.org schema with info about this app.
|
||||
announceJsonLd: {
|
||||
'@context': 'https://schema.org/',
|
||||
'@type': 'WebAPI',
|
||||
name: ipfsCoordName,
|
||||
version,
|
||||
protocol: 'generic-service',
|
||||
description:
|
||||
'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
|
||||
documentation: 'https://ipfs-service-provider.fullstack.cash/',
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: 'Permissionless Software Foundation',
|
||||
url: 'https://PSFoundation.cash'
|
||||
}
|
||||
},
|
||||
|
||||
// P2WDB webhook endpoint
|
||||
webhookService: process.env.WEBHOOKSERVICE
|
||||
? process.env.WEBHOOKSERVICE
|
||||
: 'http://localhost:5001/webhook', // P2WDB.
|
||||
|
||||
// IPFS Ports
|
||||
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001,
|
||||
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4003,
|
||||
|
||||
// BCH Mnemonic for generating encryption keys and payment address
|
||||
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
|
||||
|
||||
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 1
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
These are the environment settings for the DEVELOPMENT environment.
|
||||
This is the environment run by default with `npm start` if KOA_ENV is not
|
||||
specified.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/swap-service-dev',
|
||||
env: 'dev'
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
These are the environment settings for the PRODUCTION environment.
|
||||
This is the environment run with `npm start` if KOA_ENV=production.
|
||||
This is the environment run inside the Docker container.
|
||||
|
||||
It is assumed the MonogDB Docker container is accessed by port 5555
|
||||
so as not to conflict with the default host port of 27017 for MongoDB.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
// database: 'mongodb://172.17.0.1:5555/ipfs-service-prod',
|
||||
database: process.env.DBURL
|
||||
? process.env.DBURL
|
||||
: 'mongodb://172.17.0.1:5555/swap-service-prod',
|
||||
env: 'prod'
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
These are the environment settings for the TEST environment.
|
||||
This is the environment run with `npm start` if KOA_ENV=test.
|
||||
This is the environment run by the test suite.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/swap-service-test',
|
||||
env: 'test'
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const common = require("./env/common");
|
||||
|
||||
const env = process.env.AVAX_DEX_ENV || "development";
|
||||
const config = require(`./env/${env}`);
|
||||
|
||||
module.exports = Object.assign({}, common, config);
|
||||
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Logs</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet" media="screen" />
|
||||
<link href="/vendor/prettify.css" rel="stylesheet" media="screen" />
|
||||
<link href="/css/style.css" rel="stylesheet" media="screen, print" />
|
||||
<!-- <link href="img/favicon.ico" rel="icon" type="image/x-icon"> -->
|
||||
<script src="/vendor/polyfill.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<br />
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-4">
|
||||
<p id="outMsg"><p>
|
||||
</div>
|
||||
<div class="col-sm-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- Password for accessing logs -->
|
||||
<div class="row loginForm">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="inputLogPass" class="col-sm-2 control-label"
|
||||
>Password</label
|
||||
>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="inputLogPass"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
onclick="viewLogs()"
|
||||
>
|
||||
View Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row logTable" style="visibility: hidden;">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Level</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<tr class="tableTemplate">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/vendor/jquery.min.js"></script>
|
||||
<script src="/vendor/bootstrap.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function viewLogs() {
|
||||
try {
|
||||
const pass = $('#inputLogPass').val()
|
||||
|
||||
// if (pass === 'test') {
|
||||
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: pass
|
||||
})
|
||||
}
|
||||
const data = await fetch(`/logapi`, options)
|
||||
// console.log(`data.status: `, data.status)
|
||||
|
||||
if (data.status > 399) {
|
||||
$('#outMsg').text('Could not communicate with log server.')
|
||||
throw new Error(`Could not get log data`)
|
||||
}
|
||||
|
||||
const data2 = await data.json()
|
||||
// console.log(`data2: ${JSON.stringify(data2, null, 2)}`)
|
||||
|
||||
if (!data2.success) {
|
||||
$('#outMsg').text('Incorrect password')
|
||||
throw new Error(`Incorrect password`)
|
||||
} else {
|
||||
$('#outMsg').text('')
|
||||
}
|
||||
|
||||
$('.loginForm').css('visibility', 'hidden')
|
||||
$('.logTable').css('visibility', 'visible')
|
||||
|
||||
const logData = data2.data
|
||||
// console.log(`logData: ${JSON.stringify(logData, null, 2)}`)
|
||||
|
||||
// Clone the template row.
|
||||
const template = $('.tableTemplate')
|
||||
// debugger
|
||||
|
||||
// Loop through the array of log data.
|
||||
for (let i = 0; i < logData.length; i++) {
|
||||
const thisRow = template.clone()
|
||||
const cols = thisRow.find('td')
|
||||
|
||||
const time = new Date(logData[i].timestamp)
|
||||
// debugger
|
||||
|
||||
cols.first().text(time.toLocaleString())
|
||||
cols.next().text(logData[i].level)
|
||||
cols.next().next().text(logData[i].message)
|
||||
// debugger
|
||||
|
||||
$('.table').append(thisRow)
|
||||
}
|
||||
|
||||
|
||||
// } else {
|
||||
// console.log(`password fail`)
|
||||
// }
|
||||
} catch (err) {
|
||||
console.error(`Error in viewLogs: `, err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
const passport = require('koa-passport')
|
||||
const User = require('../src/adapters/localdb/models/users')
|
||||
const Strategy = require('passport-local')
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id)
|
||||
})
|
||||
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
try {
|
||||
const user = await User.findById(id, '-password')
|
||||
done(null, user)
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
})
|
||||
|
||||
passport.use(
|
||||
'local',
|
||||
new Strategy(
|
||||
{
|
||||
usernameField: 'email',
|
||||
passwordField: 'password'
|
||||
},
|
||||
passportCallback
|
||||
)
|
||||
)
|
||||
|
||||
async function passportCallback (email, password, done) {
|
||||
try {
|
||||
const user = await User.findOne({ email })
|
||||
if (!user) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
try {
|
||||
const isMatch = await user.validatePassword(password)
|
||||
|
||||
if (!isMatch) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
done(null, user)
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
} catch (err) {
|
||||
return done(err)
|
||||
}
|
||||
}
|
||||
|
||||
// For testing
|
||||
module.exports = { passport, passportCallback }
|
||||
@@ -0,0 +1,64 @@
|
||||
# Developer Documentation
|
||||
|
||||
This is living documentation that will be updated, edited, and changed over time, using the same version control as the rest of the code. The purpose of this documentation is to capture and explain how this ipfs-swap-service interacts with the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service), to create a permissionless, censorship-resistant database for storing trading orders. A web client will be built in the future that will interact with the REST API of this app.
|
||||
|
||||
- [Specification](./specification.md)
|
||||
|
||||
# Overview
|
||||
|
||||
There are three major pieces of software behind the ipfs-swap-service concept. They work together to form a censorship-resistant application for exchanging transaction data for building trades.
|
||||
|
||||

|
||||
|
||||
- _Client_ could be a web browser, or a command-line client like [psf-bch-wallet](https://github.com/Permissionless-Software-Foundation/psf-bch-wallet) or [psf-avax-wallet](https://github.com/Permissionless-Software-Foundation/psf-avax-wallet).
|
||||
- [ipfs-swap-service](https://github.com/christroutner/ipfs-swap-service) is the back end REST API that maintains a local database of information that the client reads from.
|
||||
- [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) is the pay-to-write global database with a REST API for interfacing with the other two pieces of software.
|
||||
|
||||
The arrows in the image represent the information flow between the three pieces of software:
|
||||
|
||||
- The _Client_ displays information about orders. It _reads_ this information from ipfs-swap-service.
|
||||
- The _Client_ is also a wallet. It can generate the needed transactions to _write_ information to the P2WDB.
|
||||
- `ipfs-swap-service` imports data from the global P2WDB database into its local database, using a [webhook](https://en.wikipedia.org/wiki/Webhook) (dashed line). It can also custody funds by creating an _Offer_ and submitting the data to the P2WDB to generate an _Order_ (solid line).
|
||||
|
||||
This architecture keeps the global database highly censorship resistant, while allowing local installations to maintain tight control over the user experience. The goal is to have many redundant copies of `ipfs-swap-service` on the network, and to empower individual traders to run their own, private copy.
|
||||
|
||||
# Back End
|
||||
|
||||
This section provides additional information on `ipfs-swap-service` and P2WDB back end software.
|
||||
|
||||
## P2WDB
|
||||
|
||||
The heart of the censorship resistance is the pay-to-write database ([P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service)). This is an [OrbitDB](https://orbitdb.org/) peer-to-peer (p2p) database. The write-access rules have been customized to allow anyone to write to the database, so long as they prove that a sufficient quantity of [PSF tokens](https://psfoundation.cash) have been burned, to pay for the write.
|
||||
|
||||
Because OrbitDB is a p2p database, no one party holds the 'official' copy of the database. Instead, like a blockchain, the database is replicated among several peers, and they coordinate updates to the database using consensus rules. Peers are free to leave or enter the network. Each peer independently verifies the database entries have sufficient proof-of-burn.
|
||||
|
||||
## `ipfs-swap-service`
|
||||
|
||||
The [ipfs-swap-service](https://github.com/christroutner/ipfs-swap-service) replicates a copy of the global P2WDB, but has the ability to apply localized filters to the data before passing it on to the _Client_, to be displayed.
|
||||
|
||||
`ipfs-swap-service` is based on this [ipfs-service-provider boilerplate](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider). It's a production-ready template for a web server, providing interfaces via REST API over HTTP, as well as JSON RPC over IPFS. It includes many features for building a web app. This includes user management and authentication, REST API and JSON RPC scaffolding, API documentation, Docker container generation, and extensive test coverage. It's intended to be customized for the needs of the website administrator.
|
||||
|
||||
- [Specification](./specification.md)
|
||||
|
||||
# Workflows
|
||||
|
||||
This section describes the protocols for the database interactions between the three main software components.
|
||||
|
||||
These are just a brief, high-level overview. Review the [Specifications](./specification.md) for more details.
|
||||
|
||||
## Writing to the Global Database
|
||||
|
||||
Adding data to the global P2WDB is a result of the interaction between the _Client_ and the P2WDB. Ideally, `ipfs-swap-service` is not involved. The [p2wdb npm library](https://www.npmjs.com/package/p2wdb) can be leveraged for easy reading and writing to the P2WDB.
|
||||
|
||||
During development, `ipfs-swap-service` is being used to submit Offers to the P2WDB and custody funds. When the project reaches maturity, these functions may be removed, and they should be handled by the Client, so that legal issues around custody of funds are not a problem.
|
||||
|
||||
Writing data follows these steps:
|
||||
|
||||
- A user submits data to the POST `/offer` REST API endpoint. This will move the funds a segregated UTXO and submit the data to the P2WDB to convert the Offer to an Order. Offers are tracked by the local instance of `ipfs-swap-service`, but Orders are tracked by all instances of `ipfs-swap-service`.
|
||||
- The P2WDB REST API will then evaluate the data and attempt to update the p2p database using the TXID.
|
||||
- Each peer on the network will independently validate the new database entry.
|
||||
- `ipfs-swap-service` will receive a webhook call to its POST `/order` endpoint. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Order model.
|
||||
|
||||
## Reading from the Local Database
|
||||
|
||||
The Client reads data from the local database stored by `ipfs-swap-service`, and does not read the global database directly. This gives `ipfs-swap-service` the opportunity to filter and modify the data locally for a more controlled user experience.
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,107 @@
|
||||
# ipfs-swap-service Specification
|
||||
|
||||
This document contains a high-level, human-readable specification for the four major architectural areas of the ipfs-swap-service:
|
||||
|
||||
- Entities
|
||||
- Use Cases
|
||||
- Controllers (inputs)
|
||||
- Adapters (outputs)
|
||||
|
||||
This reflects the [Clean Architecture](https://troutsblog.com/blog/clean-architecture) design pattern.
|
||||
|
||||
## Entities
|
||||
|
||||
Entities make up the core business concepts. If these entities change, they fundamentally change the entire app.
|
||||
|
||||
### Order
|
||||
|
||||
An order is created from data passed to the app by the P2WDB webhook.
|
||||
It is destroyed when the UTXO described in the Signal has been detected as spent.
|
||||
|
||||
Order entities have the following properties:
|
||||
|
||||
- _tokenId_ - The unique ID that identifies the class of token being offered for sale.
|
||||
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
|
||||
- _rateInSats_ - The rate in terms of tokens-per-currency-unit.
|
||||
- For Bitcoin, the min currency is sats.
|
||||
- For AVAX, the min currency is nano-Avax.
|
||||
- for eCash, the min currency is bits.
|
||||
- _minSatsToExchange_ - The minimum order size accepted.
|
||||
- _signature_ - A message signed by the address which created the order.
|
||||
- _sigMsg_ - The clear-text message used to generate the signature.
|
||||
- _utxoTxid_ - The TXID of the UTXO used in the order.
|
||||
- _utxoVout_ - The vout of the UTXO used in the order.
|
||||
- _numTokens_ - The maximum number of tokens offered for sale.
|
||||
- _timestamp_ - The ISO time when the order was created.
|
||||
- _localTimestamp_ - The localized time when the order was created.
|
||||
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB.
|
||||
- _p2wdbHash_ - The hash used to identify the order entry in the P2WDB.
|
||||
- _lokadId_ - Not used. Provided for future functionality.
|
||||
- _messageType_ - Not used. Provided for future functionality.
|
||||
- _messageClass_ - Not used. Provided for future functionality.
|
||||
|
||||
### Offer
|
||||
|
||||
An Offer Entity is nearly the same as an Order. But while an Order is generated
|
||||
by a webhook from P2WDB, the Offer Entity is created internally. It is used
|
||||
to track an Order generated by this application.
|
||||
|
||||
The Offer tracks the [HD index address](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc#hd-wallets-bip-32bip-44) used to hold tokens or BCH for sale. This is the part of the app concerned with the custody of the funds. It creates a segregated [UTXO](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc#transaction-outputs-and-inputs) to hold the offered asset. The Offer is automatically destroyed if the UTXO is accidentally spent, which is why it needs to be segregated from other wallet UTXOs.
|
||||
|
||||
Offer entities have the following properties:
|
||||
|
||||
- _offerIpfsId_ - The IPFS ID of the instance of `ipfs-swap-service` that is managing the offer.
|
||||
- _offerBchAddr_ - The BCH address controlling the offer.
|
||||
- _offerPubKey_ - The public key used to generate the BCH address, used for encryption.
|
||||
- _tokenId_ - The unique ID that identifies the class of token being offered for sale.
|
||||
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
|
||||
- _rateInSats_ - The rate in terms of tokens-per-currency-unit.
|
||||
- For Bitcoin, the min currency is sats.
|
||||
- For AVAX, the min currency is nano-Avax.
|
||||
- for eCash, the min currency is bits.
|
||||
- _minSatsToExchange_ - The minimum order size accepted.
|
||||
- _signature_ - A message signed by the address which created the order.
|
||||
- _sigMsg_ - The clear-text message used to generate the signature.
|
||||
- _utxoTxid_ - The TXID of the UTXO used in the order.
|
||||
- _utxoVout_ - The vout of the UTXO used in the order.
|
||||
- _numTokens_ - The maximum number of tokens offered for sale.
|
||||
- _timestamp_ - The ISO time when the order was created.
|
||||
- _localTimestamp_ - The localized time when the order was created.
|
||||
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB.
|
||||
- _p2wdbHash_ - The hash used to identify the order entry in the P2WDB.
|
||||
- _lokadId_ - Not used. Provided for future functionality.
|
||||
- _messageType_ - Not used. Provided for future functionality.
|
||||
- _messageClass_ - Not used. Provided for future functionality.
|
||||
|
||||
## Use Cases
|
||||
|
||||
Use cases are verbs or actions that is done _to_ an Entity or _between_ Entities.
|
||||
|
||||
### Order
|
||||
|
||||
- **`createOrder()`** - This method is triggered by a webhook from the P2WDB. It will take the data provided by the P2WDB and create a new Order entity in the local database.
|
||||
|
||||
### Offer
|
||||
|
||||
- **`ensureFunds()`** - Ensure that the wallet has enough BCH and tokens to complete the requested trade.
|
||||
- **`moveTokens()`** - Move the tokens indicated in the offer to a temporary holding address. This will generate the UTXO used in the webhook message. This function moves the funds and returns the UTXO information.
|
||||
- **`createOffer()`** - A macro command that leverages `ensureFunds()` and `moveTokens()`, to create a new Offer and submit it to the P2WDB.
|
||||
|
||||
## Controllers
|
||||
|
||||
Controllers are inputs to the system. When a controller is activated, it causes the system to react in some way.
|
||||
|
||||
### Orders
|
||||
|
||||
- **POST /order** - This POST REST API endpoint will be triggered by a webhook generated by the P2WDB. This will notify the `ipfs-swap-service` that a new entry has been added to the P2WDB that matches the `appId` of `swap-<chain>`, where `<chain>` has a value of `avax`, `bch`, or `ecash`. It's a new entry that should be evaluated for inclusion in the `ipfs-swap-service` local database.
|
||||
|
||||
### Offers
|
||||
|
||||
- **POST /offer** - This POST REST API endpoint can be triggered by the Client or a simple curl call. It passes in the data needed for `ipfs-swap-service` to generate and track a new Offer, then submit the data to the P2WDB to generate an Order that is tracked by all other instances of `ipfs-swap-service`.
|
||||
|
||||
## Adapters
|
||||
|
||||
Adapters are output libraries so that the business logic doesn't need to know any specific information about the I/O. They are essentially the output of the application.
|
||||
|
||||
- **localdb** - An adapter for the local database (MongoDB).
|
||||
- **ipfs** - An adapter for IPFS and the ipfs-coord library. It allows the app to use the JSON-RPC over IPFS.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Examples
|
||||
|
||||
Below are a series of JSON RPC calls that can be manually entered at chat.fullstack.cash to interact with the JSON RPC of this IPFS Service Provider.
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"555","method":"users","params":{ "endpoint": "createUser", "email": "test555@test.com", "name": "testy tester", "password": "password"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"556","method":"auth","params":{ "endpoint": "authUser", "login": "test555@test.com", "password": "password"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getAllUsers", "apiToken": "<JWT>"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "updateUser", "apiToken": "<JWT>", "userId": "<_id>", "name": "test999"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getUser", "apiToken": "<JWT>", "userId": "<_id>"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "deleteUser", "userId": "<_id>", "apiToken": "<JWT>"}}`
|
||||
@@ -0,0 +1,4 @@
|
||||
const Server = require('./bin/server.js')
|
||||
const server = new Server()
|
||||
|
||||
server.startServer()
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -
|
||||
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mongodb-org
|
||||
sudo service mongod start
|
||||
sudo systemctl enable mongod
|
||||
@@ -0,0 +1 @@
|
||||
This directory will hold the Winston daily logs. Any files saved to this directory will be ignored by Git.
|
||||
Generated
+43797
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "avax-dex",
|
||||
"version": "1.0.0",
|
||||
"description": "A DEX for trading tokens on the AVAX X-Chain",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "npm run test:all",
|
||||
"test:all": "export AVAX_DEX=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
||||
"test:unit": "export AVAX_DEX=test && mocha --exit --timeout 15000 --recursive test/unit/",
|
||||
"test:e2e:auto": "export AVAX_DEX=test && mocha --exit --timeout 15000 test/e2e/automated/",
|
||||
"test:temp": "export AVAX_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"coverage:report": "export AVAX_DEX=test && nyc --reporter=html mocha --exit --timeout 15000 --recursive test/unit/ test/e2e/automated/"
|
||||
},
|
||||
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
||||
"contributors": [
|
||||
"Gary Nadir"
|
||||
],
|
||||
"license": "MIT",
|
||||
"apidoc": {
|
||||
"title": "avax-dex",
|
||||
"url": "localhost:5000"
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/avax-dex",
|
||||
"dependencies": {
|
||||
"@chris.troutner/ipfs": "2.0.2",
|
||||
"@psf/bch-js": "4.20.26",
|
||||
"axios": "^0.21.4",
|
||||
"bch-message-lib": "1.13.9",
|
||||
"bcryptjs": "2.4.3",
|
||||
"glob": "7.1.6",
|
||||
"ipfs-coord": "6.7.4",
|
||||
"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",
|
||||
"koa-convert": "2.0.0",
|
||||
"koa-generic-session": "2.1.1",
|
||||
"koa-logger": "3.2.1",
|
||||
"koa-mount": "4.0.0",
|
||||
"koa-passport": "4.1.3",
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"koa2-ratelimit": "0.9.0",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "3.4.7",
|
||||
"mongoose": "5.13.13",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.4.17",
|
||||
"passport-local": "1.0.0",
|
||||
"public-ip": "^4.0.4",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "0.26.0",
|
||||
"chai": "4.3.0",
|
||||
"coveralls": "2.11.4",
|
||||
"eslint": "7.19.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-config-standard": "16.0.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"eslint-plugin-standard": "4.0.0",
|
||||
"husky": "4.3.8",
|
||||
"mocha": "8.4.0",
|
||||
"nyc": "15.1.0",
|
||||
"semantic-release": "17.4.4",
|
||||
"sinon": "9.2.4",
|
||||
"standard": "16.0.4",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"release": {
|
||||
"publish": [
|
||||
{
|
||||
"path": "@semantic-release/npm",
|
||||
"npmPublish": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/test/unit/mocks/**/*.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# Create a Dockerized API server
|
||||
#
|
||||
|
||||
#IMAGE BUILD COMMANDS
|
||||
# ct-base-ubuntu = ubuntu 18.04 + nodejs v10 LTS
|
||||
#FROM christroutner/ct-base-ubuntu
|
||||
FROM ubuntu:20.04
|
||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||
|
||||
#Update the OS and install any OS packages needed.
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||
|
||||
#Install Node and NPM
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs build-essential
|
||||
|
||||
#Create the user 'safeuser' and add them to the sudo group.
|
||||
#RUN useradd -ms /bin/bash safeuser
|
||||
#RUN adduser safeuser sudo
|
||||
|
||||
#Set password to 'password' change value below if you want a different password
|
||||
#RUN echo safeuser:password | chpasswd
|
||||
|
||||
#Set the working directory to be the home directory
|
||||
WORKDIR /home/safeuser
|
||||
|
||||
#Setup NPM for non-root global install
|
||||
#RUN mkdir /home/safeuser/.npm-global
|
||||
#RUN chown -R safeuser .npm-global
|
||||
#RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
|
||||
#RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
||||
|
||||
# Update to the latest version of npm.
|
||||
# Working with npm@7.21.1
|
||||
RUN npm install -g npm@7.23.0
|
||||
|
||||
# npm mirror to prevent direct dependency on npm.
|
||||
RUN npm set registry http://94.130.170.209:4873/
|
||||
|
||||
# Switch to user account.
|
||||
#USER safeuser
|
||||
# Prep 'sudo' commands.
|
||||
#RUN echo 'abcd8765' | sudo -S pwd
|
||||
|
||||
# Clone the rest.bitcoin.com repository
|
||||
WORKDIR /home/safeuser
|
||||
RUN git clone https://github.com/Permissionless-Software-Foundation/ipfs-service-provider
|
||||
|
||||
# Switch to the desired branch. `master` is usually stable,
|
||||
# and `stage` has the most up-to-date changes.
|
||||
WORKDIR /home/safeuser/ipfs-service-provider
|
||||
|
||||
# For development: switch to unstable branch
|
||||
RUN git checkout ct-unstable
|
||||
|
||||
# Install dependencies
|
||||
#RUN mkdir .ipfsdata
|
||||
#RUN npm install -g @mapbox/node-pre-gyp
|
||||
RUN npm install
|
||||
|
||||
# Generate the API docs
|
||||
RUN npm run docs
|
||||
|
||||
VOLUME /home/safeuser/keys
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
EXPOSE 5001
|
||||
|
||||
# Start the application.
|
||||
COPY start-production.sh start-production.sh
|
||||
CMD ["./start-production.sh"]
|
||||
|
||||
#CMD ["npm", "start"]
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Remove all untagged docker images.
|
||||
docker rmi $(docker images | grep "^<none>" | awk '{print $3}')
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Start the service with the command 'docker-compose up -d'
|
||||
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
mongo-ipfs-service:
|
||||
image: mongo
|
||||
container_name: mongo-ipfs-service
|
||||
ports:
|
||||
- '5555:27017' # <host port>:<container port>
|
||||
volumes:
|
||||
- ../data/database:/data/db
|
||||
command: mongod --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
ipfs-service:
|
||||
build: .
|
||||
container_name: ipfs-service
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
mem_limit: 500mb
|
||||
links:
|
||||
- mongo-ipfs-service
|
||||
ports:
|
||||
- '5001:5001' # <host port>:<container port>
|
||||
- '5268:5268' # IPFS TCP
|
||||
- '5269:5269' # IPFS WS
|
||||
volumes:
|
||||
- ../data/ipfsdata:/home/safeuser/ipfs-service-provider/.ipfsdata
|
||||
restart: always
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
# BEGIN: Optional configuration settings
|
||||
|
||||
# This mnemonic is used to set up persistent public key for e2ee
|
||||
# Replace this with your own 12-word mnemonic.
|
||||
# You can get one at https://wallet.fullstack.cash.
|
||||
export MNEMONIC="olive two muscle bottom coral ancient wait legend bronze useful process session"
|
||||
|
||||
# The human readable name this IPFS node identifies as.
|
||||
export COORD_NAME=ipfs-service-provider-generic
|
||||
|
||||
# Allow this node to function as a circuit relay. It must not be behind a firewall.
|
||||
#export ENABLE_CIRCUIT_RELAY=true
|
||||
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
|
||||
# and you must forward that subdomain to the IPFS_WS_PORT.
|
||||
#export CR_DOMAIN=subdomain.yourdomain.com
|
||||
|
||||
# Debug level. 0 = minimal info. 2 = max info.
|
||||
export DEBUG_LEVEL=1
|
||||
|
||||
# END: Optional configuration settings
|
||||
|
||||
|
||||
# Production database connection string.
|
||||
export DBURL=mongodb://172.17.0.1:5555/ipfs-service-prod
|
||||
|
||||
# Configure IPFS ports
|
||||
export IPFS_TCP_PORT=5268
|
||||
export IPFS_WS_PORT=5269
|
||||
|
||||
|
||||
# Configure REST API port
|
||||
export PORT=5001
|
||||
|
||||
export SVC_ENV=production
|
||||
npm start
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is an example for running a generic ipfs-service-provider instance.
|
||||
|
||||
# Ports
|
||||
export PORT=5001 # REST API port
|
||||
export IPFS_TCP_PORT=5268
|
||||
export IPFS_WS_PORT=5269
|
||||
|
||||
# The human-readible name that is used when displaying data about this node.
|
||||
export COORD_NAME=ipfs-service-provider-generic
|
||||
|
||||
# This is used for end-to-end encryption (e2ee).
|
||||
export MNEMONIC="churn aisle shield silver ladder swear hunt slim pen demand spoil veteran"
|
||||
|
||||
# 0 = less verbose. 3 = most verbose
|
||||
export DEBUG_LEVEL=1
|
||||
|
||||
# MongoDB connection string.
|
||||
#export DBURL=mongodb://localhost:27017/bch-service-dev
|
||||
|
||||
npm start
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is an example for running a ipfs-service-provider as a Circuit Relay.
|
||||
# Circuit Relays are help other nodes on the network communicate. They are
|
||||
# critical for reliable functioning of the network, and for circumventing
|
||||
# censorship.
|
||||
|
||||
# Ports
|
||||
export PORT=5001 # REST API port
|
||||
export IPFS_TCP_PORT=5268
|
||||
export IPFS_WS_PORT=5269
|
||||
|
||||
# The human-readible name that is used when displaying data about this node.
|
||||
export COORD_NAME=ipfs-service-provider-generic
|
||||
|
||||
# This is used for end-to-end encryption (e2ee).
|
||||
export MNEMONIC="churn aisle shield silver ladder swear hunt slim pen demand spoil veteran"
|
||||
|
||||
# 0 = less verbose. 3 = most verbose
|
||||
export DEBUG_LEVEL=1
|
||||
|
||||
# MongoDB connection string.
|
||||
#export DBURL=mongodb://localhost:27017/bch-service-dev
|
||||
|
||||
# Comment to disable circuit relay functionality. Or set to 1 to enable.
|
||||
export ENABLE_CIRCUIT_RELAY=1
|
||||
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
|
||||
# and you must forward that subdomain to the IPFS_WS_PORT.
|
||||
#export CR_DOMAIN=subdomain.yourdomain.com
|
||||
|
||||
npm start
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
A library for working with the system admin user. This is an auto-generated
|
||||
account with 'admin' privledges, for interacting with private APIs.
|
||||
|
||||
The admin account is regenerated every time the server is started. This improves
|
||||
security by not having stale passwords for the account. The login information
|
||||
and JWT token for the admin account is written to a JSON file, for easy
|
||||
retrieval by other apps running on the server that may need admin privledges
|
||||
to access private APIs.
|
||||
|
||||
This library is really more of an Adapter to the internal systems default
|
||||
admin user. It's not really a central Entity, which is why this library lives
|
||||
in the Adapter directory.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
const axios = require('axios').default
|
||||
const mongoose = require('mongoose')
|
||||
const User = require('../adapters/localdb/models/users')
|
||||
const config = require('../../config')
|
||||
const JsonFiles = require('../adapters/json-files')
|
||||
const jsonFiles = new JsonFiles()
|
||||
|
||||
const JSON_FILE = `system-user-${config.env}.json`
|
||||
const JSON_PATH = `${__dirname.toString()}/../../config/${JSON_FILE}`
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
const context = {}
|
||||
|
||||
let _this
|
||||
class Admin {
|
||||
constructor () {
|
||||
this.axios = axios
|
||||
this.User = User
|
||||
this.config = config
|
||||
this.jsonFiles = jsonFiles
|
||||
this.context = context
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Create the first user in the system. A 'admin' level system user that is
|
||||
// used by the Listing Manager and test scripts, in order access private API
|
||||
// functions.
|
||||
async createSystemUser () {
|
||||
// Create the system user.
|
||||
try {
|
||||
context.password = _this._randomString(20)
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'system@system.com',
|
||||
password: context.password,
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log('admin.data: ', result.data)
|
||||
|
||||
context.email = result.data.user.email
|
||||
context.id = result.data.user._id
|
||||
context.token = result.data.token
|
||||
|
||||
// Get the mongoDB entry
|
||||
const user = await _this.User.findById(context.id)
|
||||
|
||||
// Change the user type to admin
|
||||
user.type = 'admin'
|
||||
// console.log(`user created: ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
// Save the user model.
|
||||
await user.save()
|
||||
|
||||
// console.log(`admin user created: ${JSON.stringify(result.body, null, 2)}`)
|
||||
// console.log(`with password: ${context.password}`)
|
||||
|
||||
// Write out the system user information to a JSON file that external
|
||||
// applications like the Task Manager and the test scripts can access.
|
||||
|
||||
await jsonFiles.writeJSON(context, JSON_PATH)
|
||||
|
||||
return context
|
||||
} catch (err) {
|
||||
// Handle existing system user.
|
||||
if (err.response.status === 422) {
|
||||
try {
|
||||
// Delete the existing user
|
||||
await _this.deleteExistingSystemUser()
|
||||
|
||||
// Call this function again.
|
||||
return _this.createSystemUser()
|
||||
} catch (err2) {
|
||||
console.error(
|
||||
'Error in admin.js/createSystemUser() while trying generate new system user.'
|
||||
)
|
||||
// process.end(1)
|
||||
throw err2
|
||||
}
|
||||
} else {
|
||||
console.log('Error in admin.js/createSystemUser: ')
|
||||
// process.end(1)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteExistingSystemUser () {
|
||||
try {
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
|
||||
await mongoose.connect(config.database, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
|
||||
await _this.User.deleteOne({ email: 'system@system.com' })
|
||||
} catch (err) {
|
||||
console.log('Error in admin.js/deleteExistingSystemUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async loginAdmin () {
|
||||
// console.log(`loginAdmin() running.`)
|
||||
let existingUser
|
||||
|
||||
try {
|
||||
// Read the exising file
|
||||
existingUser = await _this.jsonFiles.readJSON(JSON_PATH)
|
||||
// console.log(`existingUser: ${JSON.stringify(existingUser, null, 2)}`)
|
||||
|
||||
// Log in as the user.
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
data: {
|
||||
email: 'system@system.com',
|
||||
password: existingUser.password
|
||||
}
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log(`result1: ${JSON.stringify(result, null, 2)}`)
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('Error in admin.js/loginAdmin().')
|
||||
|
||||
// console.error(`existingUser: ${JSON.stringify(existingUser, null, 2)}`)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
_randomString (length) {
|
||||
let text = ''
|
||||
const possible =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
for (let i = 0; i < length; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Admin
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
This library contains methods for working with the BCHN and BCHA blockchains.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const MsgLib = require('bch-message-lib')
|
||||
|
||||
class Bch {
|
||||
constructor () {
|
||||
// Encapsulate dependencies
|
||||
this.bchjs = new BCHJS()
|
||||
this.PSF_TOKEN_ID =
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
||||
this.msgLib = new MsgLib({ bchjs: this.bchjs })
|
||||
}
|
||||
|
||||
// Verify that the entry was signed by a specific BCH address.
|
||||
_verifySignature (verifyObj) {
|
||||
try {
|
||||
// Expand the input object.
|
||||
const { offerBchAddr, signature, sigMsg } = verifyObj
|
||||
|
||||
// Convert to BCH address.
|
||||
// const scrubbedAddr = this.bchjs.SLP.Address.toCashAddress(slpAddress)
|
||||
|
||||
const isValid = this.bchjs.BitcoinCash.verifyMessage(
|
||||
offerBchAddr,
|
||||
signature,
|
||||
sigMsg
|
||||
)
|
||||
|
||||
return isValid
|
||||
} catch (err) {
|
||||
console.error('Error in bch.js/_verifySignature()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the total psf token balance
|
||||
async getPSFTokenBalance (slpAddress) {
|
||||
try {
|
||||
if (!slpAddress || typeof slpAddress !== 'string') {
|
||||
throw new Error('slpAddress must be a string')
|
||||
}
|
||||
let psfBalance = 0
|
||||
const balances = await this.bchjs.SLP.Utils.balancesForAddress(
|
||||
slpAddress
|
||||
)
|
||||
|
||||
// Sums all the balances of all tokens
|
||||
// that match the psf token ID
|
||||
for (let i = 0; i < balances.length; i++) {
|
||||
if (balances[i].tokenId === this.PSF_TOKEN_ID) {
|
||||
psfBalance += balances[i].balance
|
||||
}
|
||||
}
|
||||
|
||||
return psfBalance
|
||||
} catch (err) {
|
||||
console.error('Error in bch.js/getPSFTokenBalance()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getMerit (slpAddr) {
|
||||
try {
|
||||
if (!slpAddr || typeof slpAddr !== 'string') {
|
||||
throw new Error('slpAddr must be a string')
|
||||
}
|
||||
const merit = await this.msgLib.merit.agMerit(slpAddr, this.PSF_TOKEN_ID)
|
||||
return merit
|
||||
} catch (error) {
|
||||
console.error('error in bch.js/getMerit()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bch
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Business logic for the /contact endpoint.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-useless-escape */
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const NodeMailer = require('../adapters/nodemailer')
|
||||
const nodemailer = new NodeMailer()
|
||||
const { wlogger } = require('../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class ContactLib {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.config = config
|
||||
_this.nodemailer = nodemailer
|
||||
}
|
||||
|
||||
async sendEmail (emailObj) {
|
||||
try {
|
||||
// Validate input
|
||||
if (!emailObj.email || typeof emailObj.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
|
||||
if (!emailObj.formMessage || typeof emailObj.formMessage !== 'string') {
|
||||
throw new Error("Property 'formMessage' must be a string!")
|
||||
}
|
||||
|
||||
// If an email list exists, the email will be sended to that list
|
||||
// otherwhise will be sended by default to the variable "_this.config.emailUser"
|
||||
let _to = [_this.config.emailUser]
|
||||
|
||||
// Email list is optional
|
||||
if (emailObj.emailList) {
|
||||
if (
|
||||
!Array.isArray(emailObj.emailList) ||
|
||||
!emailObj.emailList.length > 0
|
||||
) {
|
||||
throw new Error("Property 'emailList' must be a array of emails!")
|
||||
} else {
|
||||
_to = emailObj.emailList
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Trying send message to : ${_to}`)
|
||||
|
||||
emailObj.subject = 'Someone wants contact with you.'
|
||||
emailObj.to = _to
|
||||
|
||||
const result = await _this.nodemailer.sendEmail(emailObj)
|
||||
return result
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/contact.js/sendEmail()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = ContactLib
|
||||
@@ -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
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
This is a top-level library that encapsulates all the additional Adapters.
|
||||
The concept of Adapters comes from Clean Architecture:
|
||||
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 JSONFiles = require('./json-files')
|
||||
const FullStackJWT = require('./fullstack-jwt')
|
||||
const BCHAdapter = require('./bch')
|
||||
const WalletAdapter = require('./wallet')
|
||||
const P2wdbAdapter = require('./p2wdb')
|
||||
|
||||
//
|
||||
// // 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 bchjs = new BCHJSAdapter()
|
||||
//
|
||||
// module.exports = {
|
||||
// ipfs,
|
||||
// localdb,
|
||||
// logapi,
|
||||
// passport,
|
||||
// nodemailer,
|
||||
// wlogger,
|
||||
// jsonFiles,
|
||||
// bchjs
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
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.bch = new BCHAdapter()
|
||||
this.config = config
|
||||
this.wallet = new WalletAdapter()
|
||||
this.p2wdb = new P2wdbAdapter()
|
||||
|
||||
// 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()
|
||||
|
||||
// Open the wallet file
|
||||
const walletData = await this.wallet.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
// Instance the wallet.
|
||||
await this.wallet.instanceWallet(walletData, this.bchjs)
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/index.js/start()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Adapters
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
top-level IPFS library that combines the individual IPFS-based libraries.
|
||||
*/
|
||||
|
||||
const IpfsAdapter = require('./ipfs')
|
||||
const IpfsCoordAdapter = require('./ipfs-coord')
|
||||
|
||||
class IPFS {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.ipfsAdapter = new IpfsAdapter()
|
||||
this.IpfsCoordAdapter = IpfsCoordAdapter
|
||||
this.process = process
|
||||
|
||||
this.ipfsCoordAdapter = {} // placeholder
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
}
|
||||
|
||||
// Provides a global start() function that triggers the start() function in
|
||||
// the underlying libraries.
|
||||
async start () {
|
||||
try {
|
||||
// Start IPFS
|
||||
await this.ipfsAdapter.start()
|
||||
console.log('IPFS is ready.')
|
||||
|
||||
// this.ipfs is a Promise that will resolve into an instance of an IPFS node.
|
||||
this.ipfs = this.ipfsAdapter.ipfs
|
||||
|
||||
// Start ipfs-coord
|
||||
this.ipfsCoordAdapter = new this.IpfsCoordAdapter({
|
||||
ipfs: this.ipfs
|
||||
})
|
||||
await this.ipfsCoordAdapter.start()
|
||||
console.log('ipfs-coord is ready.')
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/ipfs/index.js/start()')
|
||||
|
||||
// If error is due to a lock file issue. Kill the process, so that
|
||||
// Docker or pm2 has a chance to restart the service.
|
||||
if (err.message.includes('Lock already being held')) {
|
||||
this.process.exit(1)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IPFS
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Clean Architecture Adapter for ipfs-coord.
|
||||
This library deals with ipfs-coord library so that the apps business logic
|
||||
doesn't need to have any specific knowledge of the library.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
const IpfsCoord = require('ipfs-coord')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const publicIp = require('public-ip')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../../config')
|
||||
// const JSONRPC = require('../../controllers/json-rpc/')
|
||||
|
||||
let _this
|
||||
|
||||
class IpfsCoordAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency injection.
|
||||
this.ipfs = localConfig.ipfs
|
||||
if (!this.ipfs) {
|
||||
throw new Error(
|
||||
'Instance of IPFS must be passed when instantiating ipfs-coord.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.IpfsCoord = IpfsCoord
|
||||
this.ipfsCoord = {}
|
||||
this.bchjs = new BCHJS()
|
||||
this.config = config
|
||||
this.publicIp = publicIp
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
async start () {
|
||||
const circuitRelayInfo = {}
|
||||
|
||||
// If configured as a Circuit Relay, get the public IP addresses for this node.
|
||||
if (this.config.isCircuitRelay) {
|
||||
try {
|
||||
const ip4 = await this.publicIp.v4()
|
||||
// const ip6 = await publicIp.v6()
|
||||
|
||||
circuitRelayInfo.ip4 = ip4
|
||||
circuitRelayInfo.tcpPort = this.config.ipfsTcpPort
|
||||
|
||||
// Domain used by browser-based secure websocket connections.
|
||||
circuitRelayInfo.crDomain = this.config.crDomain
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
}
|
||||
}
|
||||
|
||||
this.ipfsCoord = new this.IpfsCoord({
|
||||
ipfs: this.ipfs,
|
||||
type: 'node.js',
|
||||
// type: 'browser',
|
||||
bchjs: this.bchjs,
|
||||
privateLog: console.log, // Default to console.log
|
||||
isCircuitRelay: this.config.isCircuitRelay,
|
||||
circuitRelayInfo,
|
||||
apiInfo: this.config.apiInfo,
|
||||
announceJsonLd: this.config.announceJsonLd,
|
||||
debugLevel: this.config.debugLevel
|
||||
})
|
||||
|
||||
// Wait for the ipfs-coord library to signal that it is ready.
|
||||
await this.ipfsCoord.start()
|
||||
|
||||
// Signal that this adapter is ready.
|
||||
this.isReady = true
|
||||
|
||||
return this.isReady
|
||||
}
|
||||
|
||||
// Expects router to be a function, which handles the input data from the
|
||||
// pubsub channel. It's expected to be capable of routing JSON RPC commands.
|
||||
attachRPCRouter (router) {
|
||||
try {
|
||||
_this.ipfsCoord.privateLog = router
|
||||
_this.ipfsCoord.adapters.orbit.privateLog = router
|
||||
} catch (err) {
|
||||
console.error('Error in attachRPCRouter()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IpfsCoordAdapter
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Clean Architecture Adapter for IPFS.
|
||||
This library deals with IPFS so that the apps business logic doesn't need
|
||||
to have any specific knowledge of the js-ipfs library.
|
||||
|
||||
TODO: Add the external IP address to the list of multiaddrs advertised by
|
||||
this node. See this GitHub Issue for details:
|
||||
https://github.com/Permissionless-Software-Foundation/ipfs-service-provider/issues/38
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
// const IPFS = require('ipfs')
|
||||
const IPFS = require('@chris.troutner/ipfs')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../../config')
|
||||
|
||||
class IpfsAdapter {
|
||||
constructor (localConfig) {
|
||||
// Encapsulate dependencies
|
||||
this.IPFS = IPFS
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
this.config = config
|
||||
}
|
||||
|
||||
// Start an IPFS node.
|
||||
async start () {
|
||||
try {
|
||||
// Ipfs Options
|
||||
const ipfsOptions = {
|
||||
repo: './.ipfsdata/ipfs',
|
||||
start: true,
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true, // enable circuit relay dialer and listener
|
||||
hop: {
|
||||
enabled: config.isCircuitRelay // enable circuit relay HOP (make this node a relay)
|
||||
}
|
||||
},
|
||||
pubsub: true, // enable pubsub
|
||||
Swarm: {
|
||||
ConnMgr: {
|
||||
HighWater: 30,
|
||||
LowWater: 10
|
||||
}
|
||||
},
|
||||
Addresses: {
|
||||
Swarm: [
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new IPFS node.
|
||||
this.ipfs = await this.IPFS.create(ipfsOptions)
|
||||
|
||||
// Set the 'server' profile so the node does not scan private networks.
|
||||
await this.ipfs.config.profiles.apply('server')
|
||||
|
||||
// Debugging: Display IPFS config settings.
|
||||
// const configSettings = await this.ipfs.config.getAll()
|
||||
// console.log(`configSettings: ${JSON.stringify(configSettings, null, 2)}`)
|
||||
|
||||
// Signal that this adapter is ready.
|
||||
this.isReady = true
|
||||
|
||||
return this.ipfs
|
||||
} catch (err) {
|
||||
console.error('Error in ipfs.js/start()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async stop () {
|
||||
await this.ipfs.stop()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IpfsAdapter
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
A utility file for reading and writing JSON files.
|
||||
*/
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
|
||||
let _this
|
||||
|
||||
class JsonFiles {
|
||||
constructor () {
|
||||
this.fs = fs
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Writes out a JSON file of any object passed to the function.
|
||||
// This is used for testing.
|
||||
writeJSON (obj, fileName) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
if (!obj) {
|
||||
throw new Error('obj property is required')
|
||||
}
|
||||
if (!fileName || typeof fileName !== 'string') {
|
||||
throw new Error('fileName property must be a string')
|
||||
}
|
||||
const fileStr = JSON.stringify(obj, null, 2)
|
||||
|
||||
_this.fs.writeFile(fileName, fileStr, function (err) {
|
||||
if (err) {
|
||||
console.error('Error while trying to write file: ')
|
||||
throw err
|
||||
} else {
|
||||
// console.log(`${fileName} written successfully!`)
|
||||
return resolve()
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Error trying to write out object in util.js/_writeJSON().'
|
||||
)
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
readJSON (fileName) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
if (!fileName || typeof fileName !== 'string') {
|
||||
throw new Error('fileName property must be a string')
|
||||
}
|
||||
|
||||
_this.fs.readFile(fileName, (err, data) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log('.json file not found!')
|
||||
} else {
|
||||
console.log(`err: ${JSON.stringify(err, null, 2)}`)
|
||||
}
|
||||
|
||||
// throw err
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
const obj = JSON.parse(data)
|
||||
|
||||
return resolve(obj)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error trying to read JSON file in util.js/_readJSON().')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JsonFiles
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
This library encapsulates code concerned with MongoDB and Mongoose models.
|
||||
*/
|
||||
|
||||
// Load Mongoose models.
|
||||
const Users = require('./models/users')
|
||||
const Entries = require('./models/entry')
|
||||
const Order = require('./models/order')
|
||||
|
||||
class LocalDB {
|
||||
constructor () {
|
||||
// Encapsulate dependencies
|
||||
this.Users = Users
|
||||
this.Entry = Entries
|
||||
this.Order = Order
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalDB
|
||||
@@ -0,0 +1,13 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const Entry = new mongoose.Schema({
|
||||
entry: { type: String },
|
||||
description: { type: String },
|
||||
slpAddress: { type: String },
|
||||
signature: { type: String },
|
||||
category: { type: String },
|
||||
balance: { type: Number },
|
||||
merit: { type: Number }
|
||||
})
|
||||
|
||||
module.exports = mongoose.model('entry', Entry)
|
||||
@@ -0,0 +1,24 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const Offer = new mongoose.Schema({
|
||||
// SWaP Protocol Properties
|
||||
lokadId: { type: String },
|
||||
messageType: { type: Number },
|
||||
messageClass: { type: Number },
|
||||
tokenId: { type: String },
|
||||
buyOrSell: { type: String },
|
||||
rateInSats: { type: String },
|
||||
minSatsToExchange: { type: String },
|
||||
signature: { type: String },
|
||||
sigMsg: { type: String },
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
numTokens: { type: Number },
|
||||
|
||||
//
|
||||
offerIpfsId: { type: String },
|
||||
offerBchAddr: { type: String },
|
||||
offerPubKey: { type: String }
|
||||
})
|
||||
|
||||
module.exports = mongoose.model('offer', Offer)
|
||||
@@ -0,0 +1,23 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const Order = new mongoose.Schema({
|
||||
// SWaP Protocol Properties
|
||||
lokadId: { type: String },
|
||||
messageType: { type: Number },
|
||||
messageClass: { type: Number },
|
||||
tokenId: { type: String },
|
||||
buyOrSell: { type: String },
|
||||
rateInSats: { type: String },
|
||||
minSatsToExchange: { type: String },
|
||||
signature: { type: String },
|
||||
sigMsg: { type: String },
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
numTokens: { type: Number },
|
||||
timestamp: { type: String },
|
||||
localTimestamp: { type: String },
|
||||
p2wdbTxid: { type: String },
|
||||
p2wdbHash: { type: String }
|
||||
})
|
||||
|
||||
module.exports = mongoose.model('order', Order)
|
||||
@@ -0,0 +1,54 @@
|
||||
const mongoose = require('mongoose')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const config = require('../../../../config')
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
const User = new mongoose.Schema({
|
||||
type: { type: String, default: 'user' },
|
||||
name: { type: String },
|
||||
username: { type: String },
|
||||
password: { type: String, required: true },
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
}
|
||||
})
|
||||
|
||||
// Before saving, convert the password to a hash.
|
||||
User.pre('save', async function preSave (next) {
|
||||
const user = this
|
||||
|
||||
if (!user.isModified('password')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const salt = await bcrypt.genSalt(10)
|
||||
const hash = await bcrypt.hash(user.password, salt)
|
||||
|
||||
user.password = hash
|
||||
|
||||
next(null)
|
||||
})
|
||||
|
||||
// Validate the password by comparing to the saved hash.
|
||||
User.methods.validatePassword = async function validatePassword (password) {
|
||||
const user = this
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.password)
|
||||
|
||||
return isMatch
|
||||
}
|
||||
|
||||
// Generate a JWT token.
|
||||
User.methods.generateToken = function generateToken () {
|
||||
const user = this
|
||||
|
||||
const token = jwt.sign({ id: user.id }, config.token)
|
||||
// console.log(`config.token: ${config.token}`)
|
||||
// console.log(`generated token: ${token}`)
|
||||
return token
|
||||
}
|
||||
|
||||
// export default mongoose.model('user', User)
|
||||
module.exports = mongoose.model('user', User)
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Adapter for the Order database model. Provides pagination when retrieving
|
||||
orders from the datatabase.
|
||||
*/
|
||||
|
||||
const Order = require('./models/order')
|
||||
|
||||
// let _this
|
||||
|
||||
class OrderPagination {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.Order = Order
|
||||
// _this = this
|
||||
}
|
||||
|
||||
// Read all entries in the P2WDB.
|
||||
async readAll (page = 0) {
|
||||
try {
|
||||
const ENTRIES_PER_PAGE = 20
|
||||
|
||||
// Pull data from MongoDB.
|
||||
// Get all entries in the database.
|
||||
const data = await this.Order.find({})
|
||||
// Sort entries so newest entries show first.
|
||||
.sort('-timestamp')
|
||||
// Skip to the start of the selected page.
|
||||
.skip(page * ENTRIES_PER_PAGE)
|
||||
// Only return 20 results.
|
||||
.limit(ENTRIES_PER_PAGE)
|
||||
|
||||
// console.log('data: ', data)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in order-pagination.js/readAll()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderPagination
|
||||
@@ -0,0 +1,170 @@
|
||||
const lineReader = require('line-reader')
|
||||
const fs = require('fs')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
let _this
|
||||
|
||||
class LogsApi {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.fs = fs
|
||||
_this.lineReader = lineReader
|
||||
_this.config = config
|
||||
}
|
||||
|
||||
async getLogs (password) {
|
||||
try {
|
||||
// console.log('entering getLogs()')
|
||||
_this.password = password
|
||||
// console.log(`password: ${password}`)
|
||||
|
||||
// Password matches the password set in the config file.
|
||||
if (password === _this.config.logPass) {
|
||||
// Generate the full path and file name for the current log file.
|
||||
const fullPath = _this.generateFileName()
|
||||
// console.log(`fullPath: ${JSON.stringify(fullPath, null, 2)}`)
|
||||
|
||||
// Throw an error if the file does not exist.
|
||||
if (!_this.fs.existsSync(fullPath)) {
|
||||
return {
|
||||
success: false,
|
||||
data: 'file does not exist'
|
||||
}
|
||||
} else {
|
||||
// Read in the data from the log file.
|
||||
const data = await _this.readLines(fullPath)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
// Filter the logs before passing them to the front end.
|
||||
const filteredData = _this.filterLogs(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: filteredData
|
||||
}
|
||||
}
|
||||
|
||||
// Password does not match password in config file.
|
||||
} else {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in lib/logapi.js/getLogs()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Sorts the log data by their timestamp. Returns the LIMIT or less elements.
|
||||
filterLogs (data, LIMIT = 100) {
|
||||
try {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Data must be array')
|
||||
}
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
// const LIMIT = 100 // Max number of entries to return.
|
||||
|
||||
// Sort the elements by date.
|
||||
data.sort(function (a, b) {
|
||||
let dateA = new Date(a.timestamp)
|
||||
dateA = dateA.getTime()
|
||||
|
||||
let dateB = new Date(b.timestamp)
|
||||
dateB = dateB.getTime()
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
// Limit the number of elements.
|
||||
if (data.length > LIMIT) {
|
||||
return data.slice(0, LIMIT)
|
||||
}
|
||||
|
||||
// else
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in lib/logapi.js/filterLogs()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
generateFileName () {
|
||||
try {
|
||||
const now = new Date()
|
||||
let thisDate = now.getDate()
|
||||
thisDate = ('0' + thisDate).slice(-2)
|
||||
|
||||
let thisMonth = now.getMonth() + 1
|
||||
thisMonth = ('0' + thisMonth).slice(-2)
|
||||
// console.log(`thisMonth: ${thisMonth}`)
|
||||
|
||||
const thisYear = now.getFullYear()
|
||||
|
||||
const filename = `koa-${
|
||||
_this.config.env
|
||||
}-${thisYear}-${thisMonth}-${thisDate}.log`
|
||||
// console.log(`filename: ${filename}`)
|
||||
const logDir = `${__dirname.toString()}/../../logs/`
|
||||
const fullPath = `${logDir}${filename}`
|
||||
// console.log(`fullPath: ${fullPath}`)
|
||||
|
||||
return fullPath
|
||||
} catch (err) {
|
||||
console.error('Error in lib/logapi.js/generateFileName()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Promise based read-file
|
||||
/* readFile (path, opts = 'utf8') {
|
||||
return new Promise((resolve, reject) => {
|
||||
_this.fs.readFile(path, opts, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
})
|
||||
})
|
||||
} */
|
||||
|
||||
// Returns an array with each element containing a line of the file.
|
||||
readLines (filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!filename || typeof filename !== 'string') {
|
||||
throw new Error('filename must be a string')
|
||||
}
|
||||
// Throw an error if the file does not exist.
|
||||
|
||||
if (!_this.fs.existsSync(filename)) {
|
||||
throw new Error('file does not exist')
|
||||
}
|
||||
|
||||
const data = []
|
||||
|
||||
// let i = 0
|
||||
|
||||
_this.lineReader.eachLine(filename, function (line, last) {
|
||||
try {
|
||||
data.push(JSON.parse(line))
|
||||
|
||||
// Uncomment to display the raw data in each line of the winston log file.
|
||||
// console.log(`line ${i}: ${line}`)
|
||||
// i++
|
||||
|
||||
if (last) return resolve(data)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
if (last) return resolve(data)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('Error in lib/logapi.js/readLines()')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogsApi
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
A library for controlling the sending of email.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
const nodemailer = require('nodemailer')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const { wlogger } = require('./wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class NodeMailer {
|
||||
constructor () {
|
||||
this.nodemailer = nodemailer
|
||||
this.config = config
|
||||
|
||||
_this = this
|
||||
_this.transporter = _this.createTransporter()
|
||||
}
|
||||
|
||||
// Define an email server 'transport' for nodemailer
|
||||
createTransporter () {
|
||||
const transporter = _this.nodemailer.createTransport({
|
||||
host: _this.config.emailServer,
|
||||
port: 587,
|
||||
secure: false, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: _this.config.emailUser, // generated ethereal user
|
||||
pass: _this.config.emailPassword // generated ethereal password
|
||||
}
|
||||
})
|
||||
return transporter
|
||||
}
|
||||
|
||||
// Handles the sending of data via email.
|
||||
async sendEmail (data) {
|
||||
try {
|
||||
// Validate input
|
||||
if (!data.email || typeof data.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
|
||||
if (!data.to || !Array.isArray(data.to)) {
|
||||
throw new Error("Property 'to' must be a array!")
|
||||
}
|
||||
|
||||
await _this.validateEmailArray(data.to)
|
||||
|
||||
if (!data.formMessage || typeof data.formMessage !== 'string') {
|
||||
throw new Error("Property 'formMessage' must be a string!")
|
||||
}
|
||||
|
||||
if (!data.subject || typeof data.subject !== 'string') {
|
||||
throw new Error("Property 'subject' must be a string!")
|
||||
}
|
||||
|
||||
// Use the provided html or use a default html generated from the input data
|
||||
|
||||
const html = data.htmlData || _this.getHtmlFromObject(data)
|
||||
const sendObj = {
|
||||
// from: `${data.email}`, // sender address
|
||||
from: data.email,
|
||||
to: data.to, // list of receivers
|
||||
// subject: `Pearson ${subject}`, // Subject line
|
||||
subject: data.subject,
|
||||
// html: '<b>This is a test email</b>' // html body
|
||||
html
|
||||
}
|
||||
|
||||
// send mail with defined transport object
|
||||
const info = await _this.transporter.sendMail(sendObj)
|
||||
console.log('Message sent: %s', info.messageId)
|
||||
|
||||
return info
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/nodemailer.js/sendEmail()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async validateEmailArray (emailList) {
|
||||
try {
|
||||
if (!emailList || !Array.isArray(emailList)) {
|
||||
throw new Error("Property 'emailList' must be a array!")
|
||||
}
|
||||
// Email list can't be empty
|
||||
if (!emailList.length > 0) {
|
||||
throw new Error("Property 'emailList' cant be empty!")
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/nodemailer.js/validateEmailArray()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// get the email html from object
|
||||
getHtmlFromObject (objectData) {
|
||||
try {
|
||||
if (!objectData || typeof objectData !== 'object') {
|
||||
throw new Error("Property 'objectData' must be a object!")
|
||||
}
|
||||
if (!objectData.subject) {
|
||||
throw new Error("Property 'subject' must be a string!")
|
||||
}
|
||||
if (!objectData.formMessage) {
|
||||
throw new Error("Property 'formMessage' must be a string!")
|
||||
}
|
||||
|
||||
const obj = {}
|
||||
Object.assign(obj, objectData)
|
||||
|
||||
// neccesary data
|
||||
const msg = obj.formMessage.replace(/(\r\n|\n|\r)/g, '<br />')
|
||||
const now = new Date()
|
||||
const subject = obj.subject
|
||||
|
||||
// Delete unneccesary data if it exist
|
||||
delete obj.to
|
||||
delete obj.subject
|
||||
delete obj.from
|
||||
delete obj.emailList
|
||||
delete obj.formMessage
|
||||
|
||||
const bodyJson = obj
|
||||
bodyJson.message = msg
|
||||
|
||||
// Html body
|
||||
let htmlBody = ''
|
||||
|
||||
// maps the object and converts it into html format
|
||||
Object.keys(bodyJson).forEach(function (key) {
|
||||
htmlBody += `${key}: ${bodyJson[key]}<br/>`
|
||||
})
|
||||
|
||||
const defaultHtmlData = `<h3>${subject}:</h3>
|
||||
<p>
|
||||
time: ${now.toLocaleString()}<br/>
|
||||
${htmlBody}
|
||||
</p>`
|
||||
|
||||
return defaultHtmlData
|
||||
} catch (error) {
|
||||
wlogger.error('Error in lib/nodemailer.js/getHtmlFromObject()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NodeMailer
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Adapter library for interacting with the P2WDB
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const axios = require('axios')
|
||||
|
||||
// Global constants
|
||||
const P2WDB_SERVER = 'http://localhost:5001/entry/write'
|
||||
// const P2WDB_SERVER = 'https://p2wdb.fullstack.cash/entry/write'
|
||||
|
||||
class P2wdbAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.axios = axios
|
||||
}
|
||||
|
||||
async write (inputObj) {
|
||||
try {
|
||||
const { txid, signature, message, appId, data } = inputObj
|
||||
|
||||
// TODO: Input validation
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const dataObj = {
|
||||
appId,
|
||||
data,
|
||||
timestamp: now.toISOString(),
|
||||
localTimeStamp: now.toLocaleString()
|
||||
}
|
||||
|
||||
const bodyData = {
|
||||
txid,
|
||||
message,
|
||||
signature,
|
||||
data: JSON.stringify(dataObj)
|
||||
}
|
||||
|
||||
const result = await this.axios.post(P2WDB_SERVER, bodyData)
|
||||
// console.log(`Response from API: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
return result.data.hash
|
||||
} catch (err) {
|
||||
console.error('Error in p2wdb.js/write()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = P2wdbAdapter
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
koa-passport is an authorization library used for different authentication schemes.
|
||||
*/
|
||||
|
||||
const passport = require('koa-passport')
|
||||
|
||||
let _this
|
||||
class Passport {
|
||||
constructor () {
|
||||
_this = this
|
||||
this.passport = passport
|
||||
}
|
||||
|
||||
async authUser (ctx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!ctx) throw new Error('ctx is required')
|
||||
|
||||
_this.passport.authenticate('local', (err, user) => {
|
||||
try {
|
||||
if (err) throw err
|
||||
|
||||
resolve(user)
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})(ctx, null)
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Passport
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
Adapter library for working with a wallet.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const BchWallet = require('minimal-slp-wallet/index')
|
||||
|
||||
// Local libraries
|
||||
const JsonFiles = require('./json-files')
|
||||
|
||||
const WALLET_FILE = `${__dirname.toString()}/../../wallet.json`
|
||||
const PROOF_OF_BURN_QTY = 0.01
|
||||
const P2WDB_TOKEN_ID =
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
||||
|
||||
class WalletAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.jsonFiles = new JsonFiles()
|
||||
this.WALLET_FILE = WALLET_FILE
|
||||
this.BchWallet = BchWallet
|
||||
}
|
||||
|
||||
// Open the wallet file, or create one if the file doesn't exist.
|
||||
async openWallet () {
|
||||
try {
|
||||
let walletData
|
||||
|
||||
// Try to open the wallet.json file.
|
||||
try {
|
||||
// console.log('this.WALLET_FILE: ', this.WALLET_FILE)
|
||||
walletData = await this.jsonFiles.readJSON(this.WALLET_FILE)
|
||||
} catch (err) {
|
||||
// Create a new wallet file if one does not already exist.
|
||||
// console.log('caught: ', err)
|
||||
console.warn('Wallet file not found. Creating new wallet.json file.')
|
||||
|
||||
// Create a new wallet.
|
||||
// No-Update flag creates wallet without making any network calls.
|
||||
const walletInstance = new this.BchWallet(undefined, { noUpdate: true })
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await walletInstance.walletInfoPromise
|
||||
|
||||
walletData = walletInstance.walletInfo
|
||||
|
||||
// Add the nextAddress property
|
||||
walletData.nextAddress = 1
|
||||
|
||||
// Write the wallet data to the JSON file.
|
||||
await this.jsonFiles.writeJSON(walletData, this.WALLET_FILE)
|
||||
}
|
||||
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
return walletData
|
||||
} catch (err) {
|
||||
console.error('Error in openWallet()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Increments the 'nextAddress' property in the wallet file. This property
|
||||
// indicates the HD index that should be used to generate a key pair for
|
||||
// storing funds for Offers.
|
||||
// This function opens the wallet file, increments the nextAddress property,
|
||||
// then saves the change to the wallet file.
|
||||
async incrementNextAddress () {
|
||||
try {
|
||||
const walletData = await this.openWallet()
|
||||
// console.log('original walletdata: ', walletData)
|
||||
|
||||
walletData.nextAddress++
|
||||
|
||||
// console.log('walletData finish: ', walletData)
|
||||
await this.jsonFiles.writeJSON(walletData, this.WALLET_FILE)
|
||||
|
||||
// Update the working instance of the wallet.
|
||||
this.bchWallet.walletInfo.nextAddress++
|
||||
// console.log('this.bchWallet.walletInfo: ', this.bchWallet.walletInfo)
|
||||
|
||||
return walletData.nextAddress
|
||||
} catch (err) {
|
||||
console.error('Error in incrementNextAddress()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This method returns an object that contains a private key WIF, public address,
|
||||
// and the index of the HD wallet that the key pair was generated from.
|
||||
// TODO: Allow input integer. If input is used, use that as the index. If no
|
||||
// input is provided, then call incrementNextAddress().
|
||||
async getKeyPair () {
|
||||
try {
|
||||
const hdIndex = await this.incrementNextAddress()
|
||||
|
||||
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
||||
|
||||
// root seed buffer
|
||||
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
|
||||
|
||||
const masterHDNode = this.bchWallet.bchjs.HDNode.fromSeed(rootSeed)
|
||||
|
||||
// HDNode of BIP44 account
|
||||
// const account = this.bchWallet.bchjs.HDNode.derivePath(masterHDNode, "m/44'/245'/0'")
|
||||
|
||||
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||
|
||||
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
|
||||
console.log('cashAddress: ', cashAddress)
|
||||
|
||||
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
|
||||
|
||||
const outObj = {
|
||||
cashAddress,
|
||||
wif,
|
||||
hdIndex
|
||||
}
|
||||
|
||||
return outObj
|
||||
} catch (err) {
|
||||
console.error('Error in getKeyPair()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Create an instance of minimal-slp-wallet. Use data in the wallet.json file,
|
||||
// and pass the bch-js information to the minimal-slp-wallet library.
|
||||
async instanceWallet (walletData, bchjs) {
|
||||
try {
|
||||
// TODO: Throw error if bch-js is not passed in.
|
||||
// TODO: throw error if wallet data is not passed in.
|
||||
|
||||
const advancedConfig = {
|
||||
restURL: bchjs.restURL,
|
||||
apiToken: bchjs.apiToken
|
||||
}
|
||||
|
||||
// Instantiate minimal-slp-wallet.
|
||||
this.bchWallet = new this.BchWallet(walletData.mnemonic, advancedConfig)
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await this.bchWallet.walletInfoPromise
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in instanceWallet()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a cryptographic signature, required to write to the P2WDB.
|
||||
async generateSignature (message) {
|
||||
try {
|
||||
// TODO: Add input validation for message.
|
||||
|
||||
const privKey = this.bchWallet.walletInfo.privateKey
|
||||
|
||||
// console.log('privKey: ', privKey)
|
||||
// console.log('flags.data: ', flags.data)
|
||||
|
||||
const signature = this.bchWallet.bchjs.BitcoinCash.signMessageWithPrivKey(
|
||||
privKey,
|
||||
message
|
||||
)
|
||||
|
||||
return signature
|
||||
} catch (err) {
|
||||
console.error('Error in generateSignature()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Burn enough PSF to generate a valide proof-of-burn for writing to the P2WDB.
|
||||
async burnPsf () {
|
||||
try {
|
||||
// TODO: Throw error if this.bchWallet has not been instantiated.
|
||||
|
||||
// console.log('walletData: ', walletData)
|
||||
// console.log(
|
||||
// `walletData.utxos.utxoStore.slpUtxos: ${JSON.stringify(
|
||||
// walletData.utxos.utxoStore.slpUtxos,
|
||||
// null,
|
||||
// 2,
|
||||
// )}`,
|
||||
// )
|
||||
|
||||
// Get token UTXOs held by the wallet.
|
||||
const tokenUtxos = this.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens
|
||||
|
||||
// Find a token UTXO that contains PSF with a quantity higher than needed
|
||||
// to generate a proof-of-burn.
|
||||
let tokenUtxo = {}
|
||||
for (let i = 0; i < tokenUtxos.length; i++) {
|
||||
const thisUtxo = tokenUtxos[i]
|
||||
|
||||
// If token ID matches.
|
||||
if (thisUtxo.tokenId === P2WDB_TOKEN_ID) {
|
||||
if (parseFloat(thisUtxo.tokenQty) >= PROOF_OF_BURN_QTY) {
|
||||
tokenUtxo = thisUtxo
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenUtxo.tokenId !== P2WDB_TOKEN_ID) {
|
||||
throw new Error(
|
||||
`Token UTXO of with ID of ${P2WDB_TOKEN_ID} and quantity greater than ${PROOF_OF_BURN_QTY} could not be found in wallet.`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.bchWallet.burnTokens(
|
||||
PROOF_OF_BURN_QTY,
|
||||
P2WDB_TOKEN_ID
|
||||
)
|
||||
// console.log('walletData.burnTokens() result: ', result)
|
||||
|
||||
return result
|
||||
|
||||
// return {
|
||||
// success: true,
|
||||
// txid: 'fakeTxid',
|
||||
// }
|
||||
} catch (err) {
|
||||
console.error('Error in burnPsf(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WalletAdapter
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
The (Clean Architecture) Adapter for manageing a webhook connection to P2WDB.
|
||||
*/
|
||||
|
||||
const config = require('../../config')
|
||||
const axios = require('axios')
|
||||
|
||||
let _this
|
||||
|
||||
const APPID = 'swapTest555'
|
||||
|
||||
class WebHook {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.config = config
|
||||
_this.axios = axios
|
||||
}
|
||||
|
||||
// REST petition to create a webhook in p2wdb-service
|
||||
async createWebhook (url) {
|
||||
try {
|
||||
if (!url || typeof url !== 'string') {
|
||||
throw new Error('url must be a string')
|
||||
}
|
||||
|
||||
const endpoint = _this.config.webhookService
|
||||
|
||||
const obj = {
|
||||
appId: APPID,
|
||||
url
|
||||
}
|
||||
|
||||
const result = await axios.post(endpoint, obj)
|
||||
|
||||
return result.data
|
||||
} catch (err) {
|
||||
console.log('Error in adapters/webhook/createWebHook()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// REST petition to delete a webhook in p2wdb-service
|
||||
async deleteWebhook (url) {
|
||||
try {
|
||||
if (!url || typeof url !== 'string') {
|
||||
throw new Error('url must be a string')
|
||||
}
|
||||
|
||||
const endpoint = _this.config.webhookService
|
||||
|
||||
const obj = {
|
||||
appId: APPID,
|
||||
url
|
||||
}
|
||||
|
||||
const result = await axios.delete(endpoint, { data: obj })
|
||||
|
||||
return result.data
|
||||
} catch (err) {
|
||||
console.log('Error in adapters/webhook/deleteWebHook()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebHook
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Instantiates and configures the Winston logging library. This utitlity library
|
||||
can be called by other parts of the application to conveniently tap into the
|
||||
logging library.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const winston = require('winston')
|
||||
require('winston-daily-rotate-file')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
class Wlogger {
|
||||
constructor (localConfig = {}) {
|
||||
this.config = config
|
||||
|
||||
// Configure daily-rotation transport.
|
||||
this.transport = new winston.transports.DailyRotateFile({
|
||||
filename: `${__dirname.toString()}/../../logs/koa-${
|
||||
this.config.env
|
||||
}-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxSize: '1m', // 1 megabyte
|
||||
maxFiles: '5d', // 5 days
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
)
|
||||
})
|
||||
|
||||
this.transport.on('rotate', this.notifyRotation)
|
||||
|
||||
// This controls what goes into the log FILES
|
||||
this.wlogger = winston.createLogger({
|
||||
level: 'verbose',
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `combined.log`
|
||||
// - Write all logs error (and below) to `error.log`.
|
||||
//
|
||||
// new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
// new winston.transports.File({ filename: 'logs/combined.log' })
|
||||
this.transport
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
notifyRotation (oldFilename, newFilename) {
|
||||
this.wlogger.info('Rotating log files')
|
||||
}
|
||||
|
||||
outputToConsole () {
|
||||
this.wlogger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
level: 'info'
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Wlogger()
|
||||
|
||||
// Allow the logger to write to the console.
|
||||
logger.outputToConsole()
|
||||
|
||||
const wlogger = logger.wlogger
|
||||
|
||||
module.exports = { wlogger, Wlogger }
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This is a top-level library that encapsulates all the additional Controllers.
|
||||
The concept of Controllers comes from Clean Architecture:
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
|
||||
// Load the Clean Architecture Adapters library
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
// Load the JSON RPC Controller.
|
||||
const JSONRPC = require('./json-rpc')
|
||||
|
||||
// Load the Clean Architecture Use Case libraries.
|
||||
const UseCases = require('../use-cases')
|
||||
// const useCases = new UseCases({ adapters })
|
||||
|
||||
// Load the REST API Controllers.
|
||||
const RESTControllers = require('./rest-api')
|
||||
|
||||
class Controllers {
|
||||
constructor (localConfig = {}) {
|
||||
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)
|
||||
|
||||
this.attachRPCControllers()
|
||||
}
|
||||
|
||||
// Top-level function for this library.
|
||||
// Start the various Controllers and attach them to the app.
|
||||
attachRESTControllers (app) {
|
||||
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)
|
||||
}
|
||||
|
||||
// Add the JSON RPC router to the ipfs-coord adapter.
|
||||
attachRPCControllers () {
|
||||
const jsonRpcController = new JSONRPC({
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
})
|
||||
|
||||
// Attach the input of the JSON RPC router to the output of ipfs-coord.
|
||||
this.adapters.ipfs.ipfsCoordAdapter.attachRPCRouter(
|
||||
jsonRpcController.router
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Controllers
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
This is the JSON RPC router for the users API
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../../../config')
|
||||
|
||||
class AboutRPC {
|
||||
constructor (localConfig) {
|
||||
// Encapsulate dependencies
|
||||
this.jsonrpc = jsonrpc
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /about About IPFS Node
|
||||
* @apiPermission public
|
||||
* @apiName About
|
||||
* @apiGroup JSON About
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"about"}
|
||||
*
|
||||
* @apiDescription
|
||||
* This endpoint can be customized so that users can retrieve information about
|
||||
* your IPFS node and Service Provider application. This is a great place to
|
||||
* put a website URL, an IPFS hash, an other basic information.
|
||||
*/
|
||||
|
||||
// This is the top-level router for this library.
|
||||
// This is a bit different than other router libraries, because there is
|
||||
// only one response, which is a string about this node.
|
||||
async aboutRouter (rpcData) {
|
||||
return {
|
||||
success: true,
|
||||
status: 200,
|
||||
// message: aboutStr,
|
||||
message: JSON.stringify(config.announceJsonLd),
|
||||
endpoint: 'about'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AboutRPC
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
This is the JSON RPC router for the users API
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
|
||||
// Local libraries
|
||||
// const AuthLib = require('../../lib/auth')
|
||||
// const UserLib = require('../../../use-cases/user')
|
||||
const { wlogger } = require('../../../adapters/wlogger')
|
||||
const RateLimit = require('../rate-limit')
|
||||
|
||||
class AuthRPC {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Auth JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Auth JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
// this.authLib = new AuthLib()
|
||||
this.jsonrpc = jsonrpc
|
||||
this.userLib = this.useCases.user
|
||||
this.rateLimit = new RateLimit()
|
||||
}
|
||||
|
||||
// Top-level router for this library. All other methods in this class are for
|
||||
// a specific endpoint. This method routes incoming calls to one of those
|
||||
// methods.
|
||||
async authRouter (rpcData) {
|
||||
let endpoint = 'unknown'
|
||||
|
||||
try {
|
||||
// console.log('authRouter rpcData: ', rpcData)
|
||||
|
||||
endpoint = rpcData.payload.params.endpoint
|
||||
|
||||
// Route the call based on the requested endpoint.
|
||||
switch (endpoint) {
|
||||
case 'authUser':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.authUser(rpcData)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in AuthRPC/authRouter()')
|
||||
// throw err
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: 500,
|
||||
message: err.message,
|
||||
endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /auth Get JWT Token
|
||||
* @apiPermission public
|
||||
* @apiName AuthUser
|
||||
* @apiGroup JSON Auth
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"556","method":"auth","params":{ "endpoint": "authUser", "login": "test555@test.com", "password": "password"}}
|
||||
*
|
||||
* @apiParam {String} login Email(required).
|
||||
* @apiParam {String} password Password(required).
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.email User email
|
||||
* @apiSuccess {String} token JWT.
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "556",
|
||||
* "result": {
|
||||
* "method": "auth",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "endpoint": "authUser",
|
||||
* "userId": "607de52d426f3d3148b3a467",
|
||||
* "userType": "user",
|
||||
* "userName": "testy tester",
|
||||
* "userEmail": "test555@test.com",
|
||||
* "apiToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwN2RlNTJkNDI2ZjNkMzE0OGIzYTQ2NyIsImlhdCI6MTYxODg2NTcwM30.acGe5ZiBAAcbOcPQDIhvc3z0KjnuYZd1Y5pJJJC9mJQ",
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "556",
|
||||
* "result": {
|
||||
* "method": "auth",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "User not found",
|
||||
* "endpoint": "authUser"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
async authUser (rpcData) {
|
||||
try {
|
||||
// console.log('authUser rpcData: ', rpcData)
|
||||
|
||||
if (!rpcData.payload.params.login) {
|
||||
throw new Error('login must be specified')
|
||||
}
|
||||
if (!rpcData.payload.params.password) {
|
||||
throw new Error('password must be specified')
|
||||
}
|
||||
|
||||
const login = rpcData.payload.params.login
|
||||
const password = rpcData.payload.params.password
|
||||
|
||||
const user = await this.userLib.authUser(login, password)
|
||||
// console.log('user: ', user)
|
||||
|
||||
const token = user.generateToken()
|
||||
|
||||
const response = {
|
||||
endpoint: 'authUser',
|
||||
userId: user._id,
|
||||
userType: user.type,
|
||||
userName: user.name,
|
||||
userEmail: user.email,
|
||||
apiToken: token,
|
||||
status: 200,
|
||||
success: true,
|
||||
message: ''
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
// console.error('Error in authUser()')
|
||||
wlogger.error('Error in authUser(): ', err)
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'authUser'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthRPC
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
This is the parent class library for the RPC controller.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
|
||||
// Local support libraries
|
||||
const { wlogger } = require('../../adapters/wlogger')
|
||||
const UserController = require('./users')
|
||||
const AuthController = require('./auth')
|
||||
const AboutController = require('./about')
|
||||
|
||||
let _this
|
||||
|
||||
class JSONRPC {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating JSON RPC Controllers.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating JSON RPC Controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.ipfsCoord = this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord
|
||||
this.jsonrpc = jsonrpc
|
||||
this.userController = new UserController(localConfig)
|
||||
this.authController = new AuthController(localConfig)
|
||||
this.aboutController = new AboutController()
|
||||
|
||||
// Cache to store IDs of processed JSON RPC commands. Used to prevent
|
||||
// duplicate processing.
|
||||
this.msgCache = []
|
||||
this.MSG_CACHE_SIZE = 30
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// This method takes a raw string of data from IPFS, parses it, and determins
|
||||
// which controller to route the instruction to.
|
||||
async router (str, from) {
|
||||
try {
|
||||
// console.log('router str: ', str)
|
||||
console.log('JSON RPC router recieved data from: ', from)
|
||||
|
||||
// Exit quietly if 'from' is not specified.
|
||||
if (!from || typeof from !== 'string') {
|
||||
wlogger.info(
|
||||
'Warning: Can not send JSON RPC response. Can not determine which peer this message came from.'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Attempt to parse the incoming data as a JSON RPC string.
|
||||
const parsedData = _this.jsonrpc.parse(str)
|
||||
// wlogger.debug(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
|
||||
|
||||
// Exit quietly if the incoming string is an invalid JSON RPC string.
|
||||
if (parsedData.type === 'invalid') {
|
||||
wlogger.info('Rejecting invalid JSON RPC command.')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for duplicate entries with same 'id' value.
|
||||
const alreadyProcessed = _this._checkIfAlreadyProcessed(parsedData)
|
||||
if (alreadyProcessed) {
|
||||
return false
|
||||
} else {
|
||||
// console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
|
||||
|
||||
// This node will regularly ping known circuit relays with an /about
|
||||
// JSON RPC call. These will be handled by ipfs-coord, but will percolate
|
||||
// up to this library. Ignore these messages.
|
||||
if (
|
||||
parsedData.type.includes('success') &&
|
||||
parsedData.payload.method === undefined
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Log the incoming JSON RPC command.
|
||||
wlogger.info(
|
||||
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}, method: ${parsedData.payload.method}`
|
||||
)
|
||||
}
|
||||
|
||||
// Added the property "from" to the parsedData object;
|
||||
// necessary for calculating rate limits (based on the IPFS ID).
|
||||
parsedData.from = from
|
||||
|
||||
// Default return string
|
||||
let retObj = _this.defaultResponse()
|
||||
|
||||
// Route the command to the appropriate route handler.
|
||||
switch (parsedData.payload.method) {
|
||||
case 'users':
|
||||
retObj = await _this.userController.userRouter(parsedData)
|
||||
break
|
||||
case 'auth':
|
||||
retObj = await _this.authController.authRouter(parsedData)
|
||||
break
|
||||
case 'about':
|
||||
retObj = await _this.aboutController.aboutRouter(parsedData)
|
||||
}
|
||||
|
||||
// console.log('retObj: ', retObj)
|
||||
|
||||
// Convert the returned object into a JSON RPC response string.
|
||||
const retJson = _this.jsonrpc.success(parsedData.payload.id, {
|
||||
method: parsedData.payload.method,
|
||||
reciever: from,
|
||||
value: retObj
|
||||
})
|
||||
const retStr = JSON.stringify(retJson, null, 2)
|
||||
// console.log('retStr: ', retStr)
|
||||
|
||||
// Encrypt and publish the response to the originators private OrbitDB,
|
||||
// if ipfs-coord has been initialized and the peers ID is registered.
|
||||
|
||||
// console.log('responding to JSON RPC command')
|
||||
const thisNode = _this.ipfsCoord.thisNode
|
||||
// console.log('thisNode: ', thisNode)
|
||||
|
||||
try {
|
||||
await _this.ipfsCoord.useCases.peer.sendPrivateMessage(
|
||||
from,
|
||||
retStr,
|
||||
thisNode
|
||||
)
|
||||
} catch (err) {
|
||||
console.log('sendPrivateMessage() err: ', err)
|
||||
}
|
||||
|
||||
// Return the response and originator. Useful for testing.
|
||||
return { from, retStr }
|
||||
} catch (err) {
|
||||
// console.error('Error in rpc router(): ', err)
|
||||
wlogger.error('Error in rpc router(): ', err)
|
||||
// Do not throw error. This is a top-level function.
|
||||
}
|
||||
}
|
||||
|
||||
// Checks the ID of the JSON RPC call, to see if the message has already been
|
||||
// processed. Returns true if the ID exists in the cache of processed messages.
|
||||
// If the ID is new, the function adds it to the cache and return false.
|
||||
_checkIfAlreadyProcessed (data) {
|
||||
try {
|
||||
// console.log('data: ', data)
|
||||
|
||||
const id = data.payload.id
|
||||
|
||||
// Check if the hash is in the array of already processed message.
|
||||
const alreadyProcessed = this.msgCache.includes(id)
|
||||
|
||||
// Update the msgCache if this is a new message.
|
||||
if (!alreadyProcessed) {
|
||||
// Add the hash to the array.
|
||||
this.msgCache.push(id)
|
||||
|
||||
// If the array is at its max size, then remove the oldest element.
|
||||
if (this.msgCache.length > this.MSG_CACHE_SIZE) {
|
||||
this.msgCache.shift()
|
||||
}
|
||||
}
|
||||
|
||||
return alreadyProcessed
|
||||
} catch (err) {
|
||||
console.error('Error in _checkIfAlreadyProcessed: ', err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// The default JSON RPC response if the incoming command could not be routed.
|
||||
defaultResponse () {
|
||||
const errorObj = {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: 'Input does not match routing rules.'
|
||||
}
|
||||
|
||||
return errorObj
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JSONRPC
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Rate limit
|
||||
*/
|
||||
/* eslint no-useless-catch: 0 */
|
||||
|
||||
// Local libraries
|
||||
const RateLimitLib = require('koa2-ratelimit').RateLimit
|
||||
|
||||
class RateLimit {
|
||||
constructor (options) {
|
||||
// Encapsulate dependencies
|
||||
this.RateLimitLib = RateLimitLib
|
||||
|
||||
// Set default rate limit options.
|
||||
this.defaultOptions = {
|
||||
interval: { min: 1 },
|
||||
max: 60,
|
||||
onLimitReached: this.onLimitReached
|
||||
}
|
||||
|
||||
// ctx obj
|
||||
this.context = {
|
||||
state: {
|
||||
user: ''
|
||||
},
|
||||
request: {
|
||||
ip: ''
|
||||
},
|
||||
user: '',
|
||||
set: () => {}
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// `this.defaultOptions: ${JSON.stringify(this.defaultOptions, null, 2)}`
|
||||
// )
|
||||
// console.log(`options: ${JSON.stringify(options, null, 2)}`)
|
||||
|
||||
// Set rate limit settings. Default values are overwritten if user passes
|
||||
// in an options object.
|
||||
this.rateLimitOptions = Object.assign({}, this.defaultOptions, options)
|
||||
// console.log(
|
||||
// `this.rateLimitOptions: ${JSON.stringify(this.rateLimitOptions, null, 2)}`
|
||||
// )
|
||||
this.rateLimit = this.RateLimitLib.middleware(this.rateLimitOptions)
|
||||
}
|
||||
|
||||
// This function is called when the user hits their rate limits.
|
||||
onLimitReached () {
|
||||
try {
|
||||
const error = new Error() // Establish provided options as the default options.
|
||||
error.message = 'Too many requests, please try again later.'
|
||||
error.status = 429
|
||||
throw error
|
||||
} catch (error) {
|
||||
// console.log("Error in onLimitReached()", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// This is the middleware function called by the router.
|
||||
async limiter (from) {
|
||||
try {
|
||||
if (!from || typeof from !== 'string') {
|
||||
throw new Error('from must be a string')
|
||||
}
|
||||
|
||||
// Set context.limiter
|
||||
// This overrides the default koa behavior and adapts the rate limiter
|
||||
// to work with the JSON RPC over IPFS.
|
||||
this.context.state.user = from
|
||||
this.context.request.ip = from
|
||||
this.context.user = from
|
||||
|
||||
await this.rateLimit(this.context, () => {})
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error in rate-limit.js/limiter()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RateLimit
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
This is the JSON RPC router for the users API
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
|
||||
// Local libraries
|
||||
// const UserLib = require('../../../use-cases/user')
|
||||
const Validators = require('../validators')
|
||||
const RateLimit = require('../rate-limit')
|
||||
|
||||
class UserRPC {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating User JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating User JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.userLib = this.useCases.user
|
||||
this.jsonrpc = jsonrpc
|
||||
this.validators = new Validators(localConfig)
|
||||
this.rateLimit = new RateLimit()
|
||||
}
|
||||
|
||||
// Top-level router for this library. All other methods in this class are for
|
||||
// a specific endpoint. This method routes incoming calls to one of those
|
||||
// methods.
|
||||
async userRouter (rpcData) {
|
||||
let endpoint = 'unknown'
|
||||
try {
|
||||
// console.log('userRouter rpcData: ', rpcData)
|
||||
|
||||
endpoint = rpcData.payload.params.endpoint
|
||||
let user
|
||||
|
||||
// Route the call based on the value of the method property.
|
||||
switch (endpoint) {
|
||||
case 'createUser':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.createUser(rpcData)
|
||||
|
||||
case 'getAllUsers':
|
||||
await this.validators.ensureUser(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.getAll(rpcData)
|
||||
|
||||
case 'getUser':
|
||||
user = await this.validators.ensureUser(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.getUser(rpcData, user)
|
||||
|
||||
case 'updateUser':
|
||||
user = await this.validators.ensureTargetUserOrAdmin(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.updateUser(rpcData, user)
|
||||
|
||||
case 'deleteUser':
|
||||
user = await this.validators.ensureTargetUserOrAdmin(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.deleteUser(rpcData, user)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in UsersRPC/rpcRouter()')
|
||||
// throw err
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: err.status || 500,
|
||||
message: err.message,
|
||||
endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Create a new user
|
||||
* @apiPermission public
|
||||
* @apiName CreateUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"users","params":{ "endpoint": "createUser", "email": "test555@test.com", "name": "testy tester", "password": "password"}}
|
||||
*
|
||||
* @apiParam {String} email Email(required).
|
||||
* @apiParam {String} password Password(required).
|
||||
* @apiParam {String} name name or handle(optional).
|
||||
* @apiParam {string} endpoint (required)
|
||||
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "555",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "userData": {
|
||||
* "type": "user",
|
||||
* "_id": "607dd3e6426f3d3148b3a466",
|
||||
* "email": "test555@test.com",
|
||||
* "name": "testy tester",
|
||||
* "__v": 0
|
||||
* },
|
||||
* "token": "eyJhbGciOiJIUzI1NiIs1nR5cCI6IkpXVTJ9.eyJpZCI6IjYwN2RkM2U2NDI2ZjNkMzE0OGIzYTQ2NiIsImlhdCI6MTYxODg1ODk4Mn0.in4vzxDqqyCd7LpuhG3xlXeBqrJ5bp9GJPwhaoVzldI",
|
||||
* "endpoint": "createUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity",
|
||||
* "endpoint": "getUser"
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*/
|
||||
async createUser (rpcData) {
|
||||
try {
|
||||
// console.log('createUser rpcData: ', rpcData)
|
||||
|
||||
const retObj = await this.userLib.createUser(rpcData.payload.params)
|
||||
|
||||
// Add generic JSON RPC properties that every entry gets.
|
||||
retObj.endpoint = 'createUser'
|
||||
retObj.success = true
|
||||
retObj.status = 200
|
||||
retObj.message = ''
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
// console.error('Error in createUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'createUser'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Get all users
|
||||
* @apiPermission public
|
||||
* @apiName GetAllUsers
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"users","params":{ "endpoint": "getAllUsers", "apiToken": "<JWT>"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccess {Object[]} users Array of user objects
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "users": [
|
||||
* {
|
||||
* "type": "user",
|
||||
* "_id": "6070bc6da931e73d4d9e108d",
|
||||
* "email": "test678@test.com",
|
||||
* "name": "testy tester",
|
||||
* "__v": 0
|
||||
* }
|
||||
* ],
|
||||
* "endpoint": "getAllUsers",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "",
|
||||
* "endpoint": "getAllUsers"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
// Get all Users.
|
||||
async getAll () {
|
||||
try {
|
||||
const users = await this.userLib.getAllUsers()
|
||||
|
||||
return {
|
||||
users,
|
||||
endpoint: 'getAllUsers',
|
||||
success: true,
|
||||
status: 200,
|
||||
message: ''
|
||||
}
|
||||
} catch (err) {
|
||||
// console.error('Error in getAll()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'getAllUsers'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Get a user
|
||||
* @apiPermission public
|
||||
* @apiName GetAUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getUser", "apiToken": "<JWT>", "userId": "<_id>"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {String} userId (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccess {Object[]} users Array of user objects
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "user": {
|
||||
* "type": "user",
|
||||
* "_id": "607dd3e6426f3d3148b3a466",
|
||||
* "email": "test555@test.com",
|
||||
* "name": "testy tester",
|
||||
* "__v": 0
|
||||
* },
|
||||
* "endpoint": "getUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
*}
|
||||
*
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity",
|
||||
* "endpoint": "getUser"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
// Get a specific user.
|
||||
async getUser (rpcData, userModel) {
|
||||
try {
|
||||
// console.log('getUser rpcData: ', rpcData)
|
||||
|
||||
// Throw error if rpcData does not include 'userId' property for target user.
|
||||
const userId = rpcData.payload.params.userId
|
||||
|
||||
const user = await this.userLib.getUser({ id: userId })
|
||||
|
||||
return {
|
||||
user,
|
||||
endpoint: 'getUser',
|
||||
success: true,
|
||||
status: 200,
|
||||
message: ''
|
||||
}
|
||||
} catch (err) {
|
||||
// console.error('Error in getUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'getUser'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Update a user
|
||||
* @apiPermission public
|
||||
* @apiName UpdateAUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "updateUser", "apiToken": "<JWT>", "userId": "<_id>", "name": "test999"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {String} userId (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
* @apiParam {String} email Email(Optional).
|
||||
* @apiParam {String} password Password(Optional).
|
||||
* @apiParam {String} name name or handle(Optional).
|
||||
*
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name Updated name
|
||||
* @apiSuccess {String} users.username Updated username
|
||||
* @apiSuccess {String} users.email Updated email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "user": {
|
||||
* "type": "user",
|
||||
* "_id": "607dd3e6426f3d3148b3a466",
|
||||
* "email": "test555@test.com",
|
||||
* "name": "test001",
|
||||
* "__v": 0
|
||||
* },
|
||||
* "endpoint": "updateUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity.",
|
||||
* "endpoint": "updateUser"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
async updateUser (rpcData, userModel) {
|
||||
try {
|
||||
// console.log('updateUser rpcData: ', rpcData)
|
||||
|
||||
const newData = rpcData.payload.params
|
||||
|
||||
const user = await this.userLib.updateUser(userModel, newData)
|
||||
|
||||
return {
|
||||
user,
|
||||
endpoint: 'updateUser',
|
||||
success: true,
|
||||
status: 200,
|
||||
message: ''
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log('updateUser err: ', err)
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'updateUser'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Delete a user
|
||||
* @apiPermission public
|
||||
* @apiName DeleteAUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "deleteUser", "userId": "<_id>", "apiToken": "<JWT>"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {String} userId (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "endpoint": "deleteUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity",
|
||||
* "endpoint": "deleteUser"
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
*
|
||||
*/
|
||||
async deleteUser (rpcData, userModel) {
|
||||
try {
|
||||
// console.log('deleteUser rpcData: ', rpcData)
|
||||
|
||||
await this.userLib.deleteUser(userModel)
|
||||
|
||||
const retObj = {
|
||||
endpoint: 'deleteUser',
|
||||
success: true,
|
||||
status: 200,
|
||||
message: ''
|
||||
}
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
// console.error('Error in deleteUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'deleteUser'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO create deleteUser()
|
||||
}
|
||||
|
||||
module.exports = UserRPC
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Validators for the JSON RPC
|
||||
*/
|
||||
/* eslint no-useless-catch: 0 */
|
||||
|
||||
// Public npm libraries
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../../config')
|
||||
// const UserModel = require('../../adapters/localdb/models/users')
|
||||
|
||||
class Validators {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating JSON RPC Validators library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
this.jwt = jwt
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
}
|
||||
|
||||
// Returns if user passes a valid JWT token that resolves to a valid user.
|
||||
// Otherwise it throws an error.
|
||||
async ensureUser (rpcData) {
|
||||
try {
|
||||
// console.log('rpcData: ', rpcData)
|
||||
|
||||
const apiToken = rpcData.payload.params.apiToken
|
||||
if (!apiToken) throw new Error('apiToken JWT required as a parameter')
|
||||
|
||||
const decoded = this.jwt.verify(apiToken, this.config.token)
|
||||
|
||||
const user = await this.UserModel.findById(decoded.id, '-password')
|
||||
if (!user) throw new Error('User not found!')
|
||||
|
||||
return user
|
||||
} catch (err) {
|
||||
// console.error('Error in ensureUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This middleware ensures that the :id used in the API endpoint matches the
|
||||
// the ID used in the JWT, or failing that, the ID used in the JWT matches
|
||||
// an Admin user. This prevents situations like users updating other users
|
||||
// profiles or non-admins deleting users.
|
||||
async ensureTargetUserOrAdmin (rpcData) {
|
||||
try {
|
||||
// console.log('rpcData: ', rpcData)
|
||||
|
||||
// Ensure the JWT is passed in.
|
||||
const apiToken = rpcData.payload.params.apiToken
|
||||
if (!apiToken) throw new Error('apiToken JWT required as a parameter')
|
||||
|
||||
// Ensure a target user ID is provided.
|
||||
const targetUserId = rpcData.payload.params.userId
|
||||
if (!targetUserId) throw new Error('userId must be specified')
|
||||
|
||||
// Decode the JWT token.
|
||||
const decoded = this.jwt.verify(apiToken, this.config.token)
|
||||
|
||||
// Get the user described by the JWT token.
|
||||
const user = await this.UserModel.findById(decoded.id, '-password')
|
||||
if (!user) throw new Error('User not found!')
|
||||
|
||||
// If this current user is an admin, then quietly exit.
|
||||
if (user.type === 'admin') return true
|
||||
|
||||
// Throw an error if the JWT token does not match the targeted user.
|
||||
if (user._id.toString() !== targetUserId) {
|
||||
throw new Error('User is neither admin nor target user.')
|
||||
}
|
||||
|
||||
// Get the user model for the targeted User
|
||||
const targetedUser = await this.UserModel.findById(
|
||||
targetUserId,
|
||||
'-password'
|
||||
)
|
||||
|
||||
// Return the user model.
|
||||
return targetedUser
|
||||
} catch (error) {
|
||||
// console.error('Error in ensureUser()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Validators
|
||||
@@ -0,0 +1,99 @@
|
||||
const Passport = require('../../../adapters/passport')
|
||||
const passport = new Passport()
|
||||
|
||||
let _this
|
||||
|
||||
class AuthRESTController {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Auth REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Auth REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
_this = this
|
||||
this.passport = passport
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiDefine TokenError
|
||||
* @apiError Unauthorized Invalid JWT token
|
||||
*
|
||||
* @apiErrorExample {json} Unauthorized-Error:
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "status": 401,
|
||||
* "error": "Unauthorized"
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @api {post} /auth Authenticate user
|
||||
* @apiName AuthUser
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiParam {String} username User username.
|
||||
* @apiParam {String} password User password.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "username": "johndoe@gmail.com", "password": "foo" }' localhost:5000/auth
|
||||
*
|
||||
* @apiSuccess {Object} user User object
|
||||
* @apiSuccess {ObjectId} user._id User id
|
||||
* @apiSuccess {String} user.name User name
|
||||
* @apiSuccess {String} user.username User username
|
||||
* @apiSuccess {String} token Encoded JWT
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "username": "johndoe"
|
||||
* },
|
||||
* "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
|
||||
* }
|
||||
*
|
||||
* @apiError Unauthorized Incorrect credentials
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "status": 401,
|
||||
* "error": "Unauthorized"
|
||||
* }
|
||||
*/
|
||||
async authUser (ctx, next) {
|
||||
try {
|
||||
// Retrieve the user from the database after they've proven the correct
|
||||
// password.
|
||||
const user = await _this.passport.authUser(ctx, next)
|
||||
if (!user) {
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
const token = user.generateToken()
|
||||
|
||||
const response = user.toJSON()
|
||||
|
||||
delete response.password
|
||||
|
||||
ctx.body = {
|
||||
token,
|
||||
user: response
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthRESTController
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
REST API library for auth route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const AuthRESTController = require('./controller')
|
||||
|
||||
class AuthRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.authRESTController = new AuthRESTController(localConfig)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/auth'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.authRESTController.authUser)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthRouter
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Controller for the /contact REST API endpoints.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-useless-escape */
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
const ContactLib = require('../../../adapters/contact')
|
||||
const contactLib = new ContactLib()
|
||||
|
||||
let _this
|
||||
|
||||
class ContactController {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.contactLib = contactLib
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /contact/email Send Email
|
||||
* @apiName SendMail
|
||||
* @apiGroup Contact
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "obj": { "email": "email@format.com", "formMessage": "a message" } }' localhost:5001/contact/email
|
||||
*
|
||||
* @apiParam {Object} obj object (required)
|
||||
* @apiParam {String} obj.email Sender Email.
|
||||
* @apiParam {String} obj.formMessage Message.
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
*
|
||||
* success:true
|
||||
*
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async email (ctx) {
|
||||
try {
|
||||
const data = ctx.request.body
|
||||
const emailObj = data.obj
|
||||
await _this.contactLib.sendEmail(emailObj)
|
||||
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = ContactController
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
REST API library for /contact route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const ContactRESTControllerLib = require('./controller')
|
||||
|
||||
class ContactRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Contact REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Contact REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.contactRESTController = new ContactRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/contact'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/email', this.contactRESTController.email)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContactRouter
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
REST API Controller library for the /entry route
|
||||
*/
|
||||
|
||||
// const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class EntryRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /entry REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /entry REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.EntryModel = this.adapters.localdb.Entry
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// No api-doc documentation because this wont be a public endpoint
|
||||
async createEntry (ctx) {
|
||||
try {
|
||||
console.log('body: ', ctx.request.body)
|
||||
|
||||
const entryObj = ctx.request.body.entry
|
||||
|
||||
const entry = await _this.useCases.entry.createEntry(entryObj)
|
||||
|
||||
ctx.body = { entry }
|
||||
} catch (err) {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
console.log('err', err.message)
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EntryRESTControllerLib
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
REST API library for /entry route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const EntryRESTControllerLib = require('./controller')
|
||||
|
||||
let _this
|
||||
|
||||
class UserRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /entry REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /entry REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.entryRESTController = new EntryRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/entry'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.entryRESTController.createEntry)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserRouter
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
This index file for the Clean Architecture Controllers loads dependencies,
|
||||
creates instances, and attaches the controller to REST API endpoints for
|
||||
Koa.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
|
||||
// Load the REST API Controllers.
|
||||
const AuthRESTController = require('./auth')
|
||||
const UserRouter = require('./users')
|
||||
const ContactRESTController = require('./contact')
|
||||
const LogsRESTController = require('./logs')
|
||||
const EntryRouter = require('./entry')
|
||||
const OfferRouter = require('./offer')
|
||||
const OrderRouter = require('./order')
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating REST Controller libraries.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating REST Controller libraries.'
|
||||
)
|
||||
}
|
||||
|
||||
// console.log('Controllers localConfig: ', localConfig)
|
||||
}
|
||||
|
||||
attachRESTControllers (app) {
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Attach the REST API Controllers associated with the /auth route
|
||||
const authRESTController = new AuthRESTController(dependencies)
|
||||
authRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /user route
|
||||
const userRouter = new UserRouter(dependencies)
|
||||
userRouter.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /contact route
|
||||
const contactRESTController = new ContactRESTController(dependencies)
|
||||
contactRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /logs route
|
||||
const logsRESTController = new LogsRESTController(dependencies)
|
||||
logsRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /entry route
|
||||
const entryRouter = new EntryRouter(dependencies)
|
||||
entryRouter.attach(app)
|
||||
|
||||
const offerRouter = new OfferRouter(dependencies)
|
||||
offerRouter.attach(app)
|
||||
|
||||
const orderRouter = new OrderRouter(dependencies)
|
||||
orderRouter.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RESTControllers
|
||||
@@ -0,0 +1,65 @@
|
||||
const LogsApiLib = require('../../../adapters/logapi')
|
||||
const logsApiLib = new LogsApiLib()
|
||||
let _this
|
||||
|
||||
class LogsApi {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.logsApiLib = logsApiLib
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /logapi Parse and return the log files.
|
||||
* @apiPermission public
|
||||
* @apiName LogApi
|
||||
* @apiGroup Logs
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "password": "secretpasas" }' localhost:5000/logapi
|
||||
*
|
||||
* @apiParam {String} password Password (required)
|
||||
*
|
||||
* @apiSuccess {Array} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "username": "johndoe"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async getLogs (ctx) {
|
||||
try {
|
||||
// console.log('entering getLogs()')
|
||||
|
||||
// Get the user-provided password.
|
||||
const password = ctx.request.body.password
|
||||
const result = await _this.logsApiLib.getLogs(password)
|
||||
ctx.body = result
|
||||
} catch (err) {
|
||||
if (err && err.message) {
|
||||
ctx.throw(422, err.message)
|
||||
} else {
|
||||
ctx.throw(500, 'Unhandled error')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogsApi
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
REST API library for /logs route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const LogsRESTControllerLib = require('./controller')
|
||||
|
||||
class LogsRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Logs REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Logs REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
this.logsRESTController = new LogsRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/logs'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.logsRESTController.getLogs)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogsRouter
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = function errorMiddleware () {
|
||||
return async (ctx, next) => {
|
||||
try {
|
||||
await next()
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500
|
||||
ctx.body = err.message
|
||||
ctx.app.emit('error', err, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
REST API validator middleware.
|
||||
*/
|
||||
|
||||
const User = require('../../../adapters/localdb/models/users')
|
||||
const config = require('../../../../config')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class Validators {
|
||||
constructor () {
|
||||
this.User = User
|
||||
this.jwt = jwt
|
||||
this.config = config
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
async ensureUser (ctx, next) {
|
||||
try {
|
||||
// console.log(`getToken: ${typeof (getToken)}`)
|
||||
const token = _this.getToken(ctx)
|
||||
|
||||
if (!token) {
|
||||
// console.log(`Err: Token not provided.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
let decoded = null
|
||||
try {
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
decoded = _this.jwt.verify(token, config.token)
|
||||
} catch (err) {
|
||||
// console.log(`Err: Token could not be decoded: ${err}`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
ctx.state.user = await _this.User.findById(decoded.id, '-password')
|
||||
if (!ctx.state.user) {
|
||||
// console.log(`Err: Could not find user.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
// return next()
|
||||
return true
|
||||
} catch (error) {
|
||||
ctx.throw(401)
|
||||
}
|
||||
}
|
||||
|
||||
// This funciton is almost identical to ensureUser, except at the end, it verifies
|
||||
// that the 'type' associated with the user equals 'admin'.
|
||||
async ensureAdmin (ctx, next) {
|
||||
try {
|
||||
// console.log(`getToken: ${typeof (getToken)}`)
|
||||
const token = _this.getToken(ctx)
|
||||
|
||||
if (!token) {
|
||||
// console.log(`Err: Token not provided.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
let decoded = null
|
||||
try {
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
decoded = _this.jwt.verify(token, config.token)
|
||||
} catch (err) {
|
||||
// console.log(`Err: Token could not be decoded: ${err}`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
ctx.state.user = await _this.User.findById(decoded.id, '-password')
|
||||
if (!ctx.state.user) {
|
||||
// console.log(`Err: Could not find user.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
if (ctx.state.user.type !== 'admin') {
|
||||
ctx.throw(401, 'not admin')
|
||||
}
|
||||
|
||||
// return next()
|
||||
return true
|
||||
} catch (error) {
|
||||
ctx.throw(401, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// This middleware ensures that the :id used in the API endpoint matches the
|
||||
// the ID used in the JWT, or failing that, the ID used in the JWT matches
|
||||
// an Admin user. This prevents situations like users updating other users
|
||||
// profiles or non-admins deleting users.
|
||||
async ensureTargetUserOrAdmin (ctx, next) {
|
||||
try {
|
||||
// console.log(`getToken: ${typeof (getToken)}`)
|
||||
const token = _this.getToken(ctx)
|
||||
|
||||
if (!token) {
|
||||
// console.log(`Err: Token not provided.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
// The user ID targeted in this API call.
|
||||
const targetId = ctx.params.id
|
||||
// console.log(`targetId: ${JSON.stringify(targetId, null, 2)}`)
|
||||
|
||||
let decoded = null
|
||||
try {
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
decoded = _this.jwt.verify(token, config.token)
|
||||
} catch (err) {
|
||||
// console.log(`Err: Token could not be decoded: ${err}`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
ctx.state.user = await _this.User.findById(decoded.id, '-password')
|
||||
if (!ctx.state.user) {
|
||||
// console.log(`Err: Could not find user.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
// console.log('ctx.state.user: ', ctx.state.user)
|
||||
|
||||
// console.log(`ctx.state.user: ${JSON.stringify(ctx.state.user, null, 2)}`)
|
||||
// Ensure the calling user and the target user are the same.
|
||||
|
||||
if (ctx.state.user._id.toString() !== targetId.toString()) {
|
||||
wlogger.verbose(
|
||||
`Calling user and target user do not match! Calling user: ${ctx.state.user._id}, Target user: ${targetId}`
|
||||
)
|
||||
|
||||
// If they don't match, then the calling user better be an admin.
|
||||
if (ctx.state.user.type !== 'admin') {
|
||||
ctx.throw(401, 'not admin')
|
||||
} else {
|
||||
wlogger.verbose("It's ok. The user is an admin.")
|
||||
}
|
||||
}
|
||||
|
||||
// return next()
|
||||
return true
|
||||
} catch (error) {
|
||||
ctx.throw(401, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
getToken (ctx) {
|
||||
const header = ctx.request.header.authorization
|
||||
if (!header) {
|
||||
return null
|
||||
}
|
||||
const parts = header.split(' ')
|
||||
if (parts.length !== 2) {
|
||||
return null
|
||||
}
|
||||
const scheme = parts[0]
|
||||
const token = parts[1]
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
return token
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Validators
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
REST API Controller library for the /offer route
|
||||
*/
|
||||
|
||||
// const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class OfferRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /offer REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /offer REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.OfferModel = this.adapters.localdb.Offer
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// No api-doc documentation because this wont be a public endpoint
|
||||
async createOffer (ctx) {
|
||||
try {
|
||||
// console.log('body: ', ctx.request.body)
|
||||
|
||||
const offerObj = ctx.request.body.offer
|
||||
|
||||
const hash = await _this.useCases.offer.createOffer(offerObj)
|
||||
|
||||
ctx.body = { hash }
|
||||
} catch (err) {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
console.log('err', err.message)
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OfferRESTControllerLib
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
REST API library for /offer route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const OfferRESTControllerLib = require('./controller')
|
||||
|
||||
let _this
|
||||
|
||||
class OfferRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /offer REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /offer REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.offerRESTController = new OfferRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/offer'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.offerRESTController.createOffer)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OfferRouter
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
REST API Controller library for the /offer route
|
||||
*/
|
||||
|
||||
// const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class OrderRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.OrderModel = this.adapters.localdb.Order
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// No api-doc documentation because this wont be a public endpoint
|
||||
async createOrder (ctx) {
|
||||
try {
|
||||
console.log('body: ', ctx.request.body)
|
||||
|
||||
const orderObj = ctx.request.body
|
||||
|
||||
await _this.useCases.order.createOrder(orderObj)
|
||||
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
console.log('err', err.message)
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderRESTControllerLib
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
REST API library for /offer route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const OrderRESTControllerLib = require('./controller')
|
||||
|
||||
let _this
|
||||
|
||||
class OrderRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.orderRESTController = new OrderRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/order'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.orderRESTController.createOrder)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderRouter
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
REST API Controller library for the /user route
|
||||
*/
|
||||
|
||||
const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class UserRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /users REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /users REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /users Create a new user
|
||||
* @apiPermission user
|
||||
* @apiName CreateUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "user": { "email": "email@format.com", "name": "my name", "password": "secretpasas" } }' localhost:5001/users
|
||||
*
|
||||
* @apiParam {Object} user User object (required)
|
||||
* @apiParam {String} user.email Email
|
||||
* @apiParam {String} user.password Password
|
||||
* @apiParam {String} user.name name or handle
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "email": "email@format.com",
|
||||
* "password": "somestrongpassword"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async createUser (ctx) {
|
||||
try {
|
||||
const userObj = ctx.request.body.user
|
||||
|
||||
const { userData, token } = await _this.useCases.user.createUser(userObj)
|
||||
// console.log('userData: ', userData)
|
||||
// console.log('token: ', token)
|
||||
|
||||
ctx.body = {
|
||||
user: userData,
|
||||
token
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /users Get all users
|
||||
* @apiPermission user
|
||||
* @apiName GetUsers
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5000/users
|
||||
*
|
||||
* @apiSuccess {Object[]} users Array of user objects
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "users": [{
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "email": "email@format.com"
|
||||
* }]
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async getUsers (ctx) {
|
||||
try {
|
||||
const users = await _this.useCases.user.getAllUsers()
|
||||
|
||||
ctx.body = { users }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in users/controller.js/getUsers(): '.err)
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /users/:id Get user by id
|
||||
* @apiPermission user
|
||||
* @apiName GetUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5000/users/56bd1da600a526986cf65c80
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "email": "email@format.com"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async getUser (ctx, next) {
|
||||
try {
|
||||
const user = await _this.useCases.user.getUser(ctx.params)
|
||||
|
||||
ctx.body = {
|
||||
user
|
||||
}
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
|
||||
if (next) {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {put} /users/:id Update a user
|
||||
* @apiPermission user
|
||||
* @apiName UpdateUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X PUT -d '{ "user": { "name": "Cool new Name" } }' localhost:5000/users/56bd1da600a526986cf65c80
|
||||
*
|
||||
* @apiParam {Object} user User object (required)
|
||||
* @apiParam {String} user.name Name.
|
||||
* @apiParam {String} user.email Email.
|
||||
* @apiParam {String} user.password Password. (optional)
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name Updated name
|
||||
* @apiSuccess {String} users.username Updated username
|
||||
* @apiSuccess {String} users.email Updated email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "Cool new name"
|
||||
* "email": "email@format.com"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async updateUser (ctx) {
|
||||
try {
|
||||
const existingUser = ctx.body.user
|
||||
const newData = ctx.request.body.user
|
||||
|
||||
const user = await _this.useCases.user.updateUser(existingUser, newData)
|
||||
|
||||
ctx.body = {
|
||||
user
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {delete} /users/:id Delete a user
|
||||
* @apiPermission user
|
||||
* @apiName DeleteUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X DELETE localhost:5000/users/56bd1da600a526986cf65c80
|
||||
*
|
||||
* @apiSuccess {StatusCode} 200
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "success": true
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async deleteUser (ctx) {
|
||||
try {
|
||||
const user = ctx.body.user
|
||||
|
||||
// await user.remove()
|
||||
await _this.useCases.user.deleteUser(user)
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserRESTControllerLib
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
REST API library for /user route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const UserRESTControllerLib = require('./controller')
|
||||
const Validators = require('../middleware/validators')
|
||||
|
||||
let _this
|
||||
|
||||
class UserRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.userRESTController = new UserRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/users'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.userRESTController.createUser)
|
||||
this.router.get('/', this.getAll)
|
||||
this.router.get('/:id', this.getById)
|
||||
this.router.put('/:id', this.updateUser)
|
||||
this.router.delete('/:id', this.deleteUser)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
|
||||
async getAll (ctx, next) {
|
||||
await _this.validators.ensureUser(ctx, next)
|
||||
await _this.userRESTController.getUsers(ctx, next)
|
||||
}
|
||||
|
||||
async getById (ctx, next) {
|
||||
await _this.validators.ensureUser(ctx, next)
|
||||
await _this.userRESTController.getUser(ctx, next)
|
||||
}
|
||||
|
||||
async updateUser (ctx, next) {
|
||||
await _this.validators.ensureTargetUserOrAdmin(ctx, next)
|
||||
await _this.userRESTController.getUser(ctx, next)
|
||||
await _this.userRESTController.updateUser(ctx, next)
|
||||
}
|
||||
|
||||
async deleteUser (ctx, next) {
|
||||
await _this.validators.ensureTargetUserOrAdmin(ctx, next)
|
||||
await _this.userRESTController.getUser(ctx, next)
|
||||
await _this.userRESTController.deleteUser(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserRouter
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Entry Entity
|
||||
*/
|
||||
class Entry {
|
||||
validate ({
|
||||
entry,
|
||||
description,
|
||||
slpAddress,
|
||||
signature,
|
||||
category
|
||||
} = {}) {
|
||||
// Input Validation
|
||||
if (!entry || typeof entry !== 'string') {
|
||||
throw new Error("Property 'entry' must be a string!")
|
||||
}
|
||||
if (!description || typeof description !== 'string') {
|
||||
throw new Error("Property 'description' must be a string!")
|
||||
}
|
||||
if (!slpAddress || typeof slpAddress !== 'string') {
|
||||
throw new Error("Property 'slpAddress' must be a string!")
|
||||
}
|
||||
if (!signature || typeof signature !== 'string') {
|
||||
throw new Error("Property 'signature' must be a string!")
|
||||
}
|
||||
if (!category || typeof category !== 'string') {
|
||||
throw new Error("Property 'category' must be a string!")
|
||||
}
|
||||
|
||||
const entryData = {
|
||||
entry,
|
||||
description,
|
||||
slpAddress,
|
||||
signature,
|
||||
category
|
||||
}
|
||||
|
||||
return entryData
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Entry
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Offer Entity
|
||||
An Offer Entity is nearly the same as an Order. But while an Order is generated
|
||||
by a webhook from P2WDB, the Offer Entity is created internally. It is used
|
||||
to track an Order generated by this application.
|
||||
|
||||
The Offer tracks the hdIndex address used to hold tokens or BCH for sale.
|
||||
*/
|
||||
class Offer {
|
||||
validate (data) {
|
||||
const {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens
|
||||
} = data
|
||||
|
||||
// Input Validation
|
||||
if (!messageType || typeof messageType !== 'number') {
|
||||
throw new Error("Property 'messageType' must be an integer number.")
|
||||
}
|
||||
if (!messageClass || typeof messageClass !== 'number') {
|
||||
throw new Error("Property 'messageClass' must be an integer number.")
|
||||
}
|
||||
if (!tokenId || typeof tokenId !== 'string') {
|
||||
throw new Error("Property 'tokenId' must be a string.")
|
||||
}
|
||||
if (!buyOrSell || typeof buyOrSell !== 'string') {
|
||||
throw new Error("Property 'buyOrSell' must be a string.")
|
||||
}
|
||||
if (!rateInSats || typeof rateInSats !== 'number') {
|
||||
throw new Error("Property 'rateInSats' must be an integer number.")
|
||||
}
|
||||
if (!minSatsToExchange || typeof minSatsToExchange !== 'number') {
|
||||
throw new Error("Property 'minSatsToExchange' must be an integer number.")
|
||||
}
|
||||
if (!numTokens || typeof numTokens !== 'number') {
|
||||
throw new Error("Property 'numTokens' must be a number.")
|
||||
}
|
||||
|
||||
const offerData = {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens
|
||||
}
|
||||
|
||||
return offerData
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Offer
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Order Entity
|
||||
An order is created when a new Signal is detected via the P2WDB webhook.
|
||||
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
||||
|
||||
{
|
||||
lokadId: 'SWP', // Placeholder for now, use for backwards compatibility
|
||||
messageType: 1, // SLP Atomic Swap
|
||||
messageClass: 1,
|
||||
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 7972, // Price per token in sats
|
||||
minSatsToExchange: 8100, // Minum size of UTXO to use, e.g. 1 token + miner fees
|
||||
|
||||
// Signature from the address holding the tokens, provides 'proof of reserves'
|
||||
signature: 'H2Sq0UPh0jgs1Zt3JERHtbzfPGXJk9DgJ0FVxVa6iUqiIh6XcvEUFBbvYIuODQs3hYSCkkjcuzbvzNEiv69kFKg=',
|
||||
sigMsg: 'test',
|
||||
address: 'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8',
|
||||
|
||||
// UTXO for sale
|
||||
utxoTxid: 'b9457808be70c39a9cc6c5857cbef856b35fdc91a59debfe06acfc45b11955e3',
|
||||
utxoVout: 2
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Order Entity
|
||||
An order is created when a new Signal is detected via the P2WDB webhook.
|
||||
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
||||
*/
|
||||
class OrderEntity {
|
||||
validate (orderData = {}) {
|
||||
// Throw an error if input object does not have a data property
|
||||
if (!orderData.data) {
|
||||
throw new Error(
|
||||
'Input to order.validate() must be an object with a data property.'
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens,
|
||||
utxoTxid,
|
||||
utxoVout
|
||||
} = orderData.data
|
||||
|
||||
// Input Validation
|
||||
if (!messageType || typeof messageType !== 'number') {
|
||||
throw new Error("Property 'messageType' must be an integer number.")
|
||||
}
|
||||
if (!messageClass || typeof messageClass !== 'number') {
|
||||
throw new Error("Property 'messageClass' must be an integer number.")
|
||||
}
|
||||
if (!tokenId || typeof tokenId !== 'string') {
|
||||
throw new Error("Property 'tokenId' must be a string.")
|
||||
}
|
||||
if (!buyOrSell || typeof buyOrSell !== 'string') {
|
||||
throw new Error("Property 'buyOrSell' must be a string.")
|
||||
}
|
||||
if (!rateInSats || typeof rateInSats !== 'number') {
|
||||
throw new Error("Property 'rateInSats' must be an integer number.")
|
||||
}
|
||||
if (!minSatsToExchange || typeof minSatsToExchange !== 'number') {
|
||||
throw new Error("Property 'minSatsToExchange' must be an integer number.")
|
||||
}
|
||||
if (!numTokens || typeof numTokens !== 'number') {
|
||||
throw new Error("Property 'numTokens' must be a number.")
|
||||
}
|
||||
if (!utxoTxid || typeof utxoTxid !== 'string') {
|
||||
throw new Error("Property 'utxoTxid' must be a string.")
|
||||
}
|
||||
if (typeof utxoVout !== 'number') {
|
||||
throw new Error("Property 'utxoVout' must be an integer number.")
|
||||
}
|
||||
|
||||
const validatedOrderData = {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens,
|
||||
utxoTxid,
|
||||
utxoVout,
|
||||
timestamp: orderData.timestamp,
|
||||
localTimestamp: orderData.localTimeStamp,
|
||||
txid: orderData.txid,
|
||||
p2wdbHash: orderData.hash
|
||||
}
|
||||
|
||||
return validatedOrderData
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderEntity
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
User Entity
|
||||
*/
|
||||
|
||||
class User {
|
||||
validate ({ name, email, password } = {}) {
|
||||
// Input Validation
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
if (!password || typeof password !== 'string') {
|
||||
throw new Error("Property 'password' must be a string!")
|
||||
}
|
||||
if (!name || typeof name !== 'string') {
|
||||
throw new Error("Property 'name' must be a string!")
|
||||
}
|
||||
|
||||
const userData = { name, email, password }
|
||||
|
||||
return userData
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
@@ -0,0 +1,66 @@
|
||||
const { wlogger } = require('../adapters/wlogger')
|
||||
|
||||
const EntryEntiy = require('../entities/entry')
|
||||
class EntryLib {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating User Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.EntryEntity = new EntryEntiy()
|
||||
this.EntryModel = this.adapters.localdb.Entry
|
||||
this.bch = this.adapters.bch
|
||||
}
|
||||
|
||||
// Create a new entry model and add it to the Mongo database.
|
||||
async createEntry (entryObj) {
|
||||
try {
|
||||
// Input Validation
|
||||
const entryEntity = this.EntryEntity.validate(entryObj)
|
||||
|
||||
// Verify that the entry was signed by a specific BCH address.
|
||||
const isValidSignature = this.bch._verifySignature(entryEntity)
|
||||
if (!isValidSignature) {
|
||||
throw new Error('Invalid signature')
|
||||
}
|
||||
|
||||
// Verify psf tokens balance
|
||||
|
||||
const psfBalance = await this.bch.getPSFTokenBalance(
|
||||
entryEntity.slpAddress
|
||||
)
|
||||
|
||||
if (psfBalance < 10) {
|
||||
throw new Error('Insufficient psf balance')
|
||||
}
|
||||
|
||||
const merit = await this.bch.getMerit(entryEntity.slpAddress)
|
||||
|
||||
const updatedEntry = {
|
||||
entry: entryEntity.entry.trim(),
|
||||
slpAddress: entryEntity.slpAddress.trim(),
|
||||
description: entryEntity.description.trim(),
|
||||
signature: entryEntity.signature.trim(),
|
||||
category: entryEntity.category.trim(),
|
||||
balance: psfBalance,
|
||||
merit
|
||||
}
|
||||
|
||||
const entryModel = new this.EntryModel(updatedEntry)
|
||||
await entryModel.save()
|
||||
|
||||
return entryModel
|
||||
} catch (err) {
|
||||
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
|
||||
wlogger.error('Error in use-cases/entry.js/createEntry()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EntryLib
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
This is a top-level library that encapsulates all the additional Use Cases.
|
||||
The concept of Use Cases comes from Clean Architecture:
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
const UserUseCases = require('./user')
|
||||
const EntryUseCases = require('./entry')
|
||||
const OfferUseCases = require('./offer')
|
||||
const OrderUseCases = require('./order')
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// console.log('use-cases/index.js localConfig: ', localConfig)
|
||||
this.user = new UserUseCases(localConfig)
|
||||
this.entry = new EntryUseCases(localConfig)
|
||||
this.offer = new OfferUseCases(localConfig)
|
||||
this.order = new OrderUseCases(localConfig)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UseCases
|
||||
@@ -0,0 +1,148 @@
|
||||
const { wlogger } = require('../adapters/wlogger')
|
||||
|
||||
const OfferEntity = require('../entities/offer')
|
||||
|
||||
class OfferLib {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating Offer Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.offerEntity = new OfferEntity()
|
||||
this.OfferModel = this.adapters.localdb.Offer
|
||||
this.bch = this.adapters.bch
|
||||
}
|
||||
|
||||
// Create a new offer model and add it to the Mongo database.
|
||||
async createOffer (entryObj) {
|
||||
try {
|
||||
// console.log('createOffer(entryObj): ', entryObj)
|
||||
|
||||
// Input Validation
|
||||
const offerEntity = this.offerEntity.validate(entryObj)
|
||||
// console.log('offerEntity: ', offerEntity)
|
||||
|
||||
// Ensure sufficient tokens exist to create the offer.
|
||||
await this.ensureFunds(offerEntity)
|
||||
|
||||
// Move the tokens to holding address.
|
||||
const utxoInfo = await this.moveTokens(offerEntity)
|
||||
console.log('utxoInfo: ', utxoInfo)
|
||||
|
||||
// Update the UTXO store for the wallet.
|
||||
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000)
|
||||
await this.adapters.wallet.bchWallet.getUtxos()
|
||||
|
||||
// Update the offer with the new UTXO information.
|
||||
offerEntity.utxoTxid = utxoInfo.txid
|
||||
offerEntity.utxoVout = utxoInfo.vout
|
||||
|
||||
// Burn PSF token to pay for P2WDB write.
|
||||
const txid = await this.adapters.wallet.burnPsf()
|
||||
console.log('burn txid: ', txid)
|
||||
console.log(`https://simpleledger.info/tx/${txid}`)
|
||||
|
||||
// generate signature.
|
||||
const now = new Date()
|
||||
const message = now.toISOString()
|
||||
const signature = await this.adapters.wallet.generateSignature(message)
|
||||
// console.log('signature: ', signature)
|
||||
|
||||
const p2wdbObj = {
|
||||
txid,
|
||||
signature,
|
||||
message,
|
||||
appId: 'swapTest555',
|
||||
data: offerEntity
|
||||
}
|
||||
|
||||
// Add offer to P2WDB.
|
||||
const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
||||
// console.log('hash: ', hash)
|
||||
|
||||
return hash
|
||||
} catch (err) {
|
||||
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
|
||||
wlogger.error('Error in use-cases/entry.js/createOffer())')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Move the tokens indicated in the offer to a temporary holding address.
|
||||
// This will generate the UTXO used in the Signal message. This function
|
||||
// moves the funds and returns the UTXO information.
|
||||
async moveTokens (offerEntity) {
|
||||
try {
|
||||
const keyPair = await this.adapters.wallet.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
|
||||
const receiver = {
|
||||
address: keyPair.cashAddress,
|
||||
tokenId: offerEntity.tokenId,
|
||||
qty: offerEntity.numTokens
|
||||
}
|
||||
|
||||
const txid = await this.adapters.wallet.bchWallet.sendTokens(receiver, 3)
|
||||
|
||||
const utxoInfo = {
|
||||
txid,
|
||||
vout: 0
|
||||
}
|
||||
|
||||
return utxoInfo
|
||||
} catch (err) {
|
||||
console.error('Error in moveTokens(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the wallet has enough BCH and tokens to complete the requested
|
||||
// trade.
|
||||
async ensureFunds (offerEntity) {
|
||||
try {
|
||||
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
|
||||
|
||||
// Get UTXOs.
|
||||
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
|
||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
if (offerEntity.buyOrSell.includes('sell')) {
|
||||
// Sell Offer
|
||||
|
||||
// Get token UTXOs that match the token in the offer.
|
||||
const tokenUtxos = utxos.slpUtxos.type1.tokens.filter(
|
||||
(x) => x.tokenId === offerEntity.tokenId
|
||||
)
|
||||
// console.log('tokenUtxos: ', tokenUtxos)
|
||||
|
||||
// Get the total amount of tokens in the wallet that match the token
|
||||
// in the offer.
|
||||
let totalTokenBalance = 0
|
||||
tokenUtxos.map((x) => (totalTokenBalance += parseFloat(x.tokenQty)))
|
||||
// console.log('totalTokenBalance: ', totalTokenBalance)
|
||||
|
||||
// If there are fewer tokens in the wallet than what's in the offer,
|
||||
// throw an error.
|
||||
if (totalTokenBalance <= offerEntity.numTokens) {
|
||||
throw new Error(
|
||||
'App wallet does not have enough tokens to satisfy the SELL offer.'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Buy Offer
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in ensureFunds()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OfferLib
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Use Case library for Orders.
|
||||
Orders are created by a webhook trigger from the P2WDB. Orders are a result of
|
||||
new data in P2WDB. They differ from Offers, which are generated by a local
|
||||
user.
|
||||
An Order is created to match a local Offer, but it's created indirectly, as
|
||||
a response to the webhook from the P2WDB. In this way, Orders generated from
|
||||
local Offers are no different than Orders generated by other peers.
|
||||
*/
|
||||
|
||||
const OrderEntity = require('../../entities/order')
|
||||
|
||||
class OrderUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating Order Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
this.orderEntity = new OrderEntity()
|
||||
this.OrderModel = this.adapters.localdb.Order
|
||||
}
|
||||
|
||||
// This method is called by the POST /order REST API controller, which is
|
||||
// triggered by a P2WDB webhook.
|
||||
async createOrder (orderObj) {
|
||||
try {
|
||||
console.log('Use Case createOrder(orderObj): ', orderObj)
|
||||
|
||||
// console.log('this.adapters.bchjs: ', this.adapters.bchjs)
|
||||
|
||||
// Verify that UTXO in order is unspent. If it is spent, then ignore the
|
||||
// order.
|
||||
const txid = orderObj.data.utxoTxid
|
||||
const vout = orderObj.data.utxoVout
|
||||
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
||||
txid,
|
||||
vout
|
||||
)
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
if (utxoStatus === null) return false
|
||||
|
||||
const orderEntity = this.orderEntity.validate(orderObj)
|
||||
console.log('orderEntity: ', orderEntity)
|
||||
|
||||
// Add order to the local database.
|
||||
const orderModel = new this.OrderModel(orderEntity)
|
||||
await orderModel.save()
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in createOrder()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderUseCases
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
This library leverages the p-retry and p-queue libraries, to create a
|
||||
validation queue with automatic retry.
|
||||
|
||||
New nodes syncing will attempt to rapidly validate a lot of entries.
|
||||
A promise-based queue allows this to happen while respecting rate-limits
|
||||
of the blockchain service provider.
|
||||
|
||||
pay-to-write-access-controller.js depends on this library.
|
||||
*/
|
||||
|
||||
const PQueue = require('p-queue').default
|
||||
const pRetry = require('p-retry')
|
||||
|
||||
let _this
|
||||
|
||||
class RetryQueue {
|
||||
constructor (localConfig = {}) {
|
||||
if (!localConfig.bchjs) {
|
||||
throw new Error(
|
||||
'Must pass instance of bch-js when instantiating RetryQueue Class.'
|
||||
)
|
||||
}
|
||||
this.bchjs = localConfig.bchjs
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.validationQueue = new PQueue({ concurrency: 1 })
|
||||
this.pRetry = pRetry
|
||||
|
||||
this.attempts = 5
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Add an async function to the queue, and execute it with the input object.
|
||||
async addToQueue (funcHandle, inputObj) {
|
||||
try {
|
||||
console.log('addToQueue inputObj: ', inputObj)
|
||||
|
||||
if (!funcHandle) {
|
||||
throw new Error('function handler is required')
|
||||
}
|
||||
if (!inputObj) {
|
||||
throw new Error('input object is required')
|
||||
}
|
||||
|
||||
const returnVal = await _this.validationQueue.add(() =>
|
||||
_this.retryWrapper(funcHandle, inputObj)
|
||||
)
|
||||
return returnVal
|
||||
} catch (err) {
|
||||
console.error('Error in addToQueue(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the p-retry library.
|
||||
// This function returns a promise that will resolve to the output of the
|
||||
// function 'funcHandle'.
|
||||
async retryWrapper (funcHandle, inputObj) {
|
||||
try {
|
||||
console.log('retryWrapper inputObj: ', inputObj)
|
||||
|
||||
if (!funcHandle) {
|
||||
throw new Error('function handler is required')
|
||||
}
|
||||
if (!inputObj) {
|
||||
throw new Error('input object is required')
|
||||
}
|
||||
console.log('Entering retryWrapper()')
|
||||
|
||||
// Add artificial delay to prevent 429 errors.
|
||||
await this.bchjs.Util.sleep(2000)
|
||||
|
||||
return this.pRetry(
|
||||
async () => {
|
||||
return await funcHandle(inputObj)
|
||||
},
|
||||
{
|
||||
onFailedAttempt: _this.handleValidationError,
|
||||
retries: this.attempts // Retry 5 times
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Error in retryWrapper: ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Notifies the user that an error occured and that a retry will be attempted.
|
||||
// It tracks the number of retries until it fails.
|
||||
async handleValidationError (error) {
|
||||
try {
|
||||
const errorMsg = `Attempt ${error.attemptNumber} to validate entry. There are ${error.retriesLeft} retries left. Waiting before trying again.`
|
||||
console.log(errorMsg)
|
||||
const SLEEP_TIME = 30000
|
||||
console.log(`Waiting ${SLEEP_TIME} milliseconds before trying again.\n`)
|
||||
await _this.bchjs.Util.sleep(SLEEP_TIME) // 30 sec
|
||||
} catch (err) {
|
||||
console.error('Error in handleValidationError(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RetryQueue
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
This library contains business-logic for dealing with users. Most of these
|
||||
functions are called by the /user REST API endpoints.
|
||||
*/
|
||||
|
||||
const UserEntity = require('../entities/user')
|
||||
const { wlogger } = require('../adapters/wlogger')
|
||||
|
||||
class UserLib {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating User Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.UserEntity = new UserEntity()
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
}
|
||||
|
||||
// Create a new user model and add it to the Mongo database.
|
||||
async createUser (userObj) {
|
||||
try {
|
||||
// Input Validation
|
||||
|
||||
const userEntity = this.UserEntity.validate(userObj)
|
||||
const user = new this.UserModel(userEntity)
|
||||
|
||||
// Enforce default value of 'user'
|
||||
user.type = 'user'
|
||||
// console.log('user: ', user)
|
||||
|
||||
// Save the new user model to the database.
|
||||
await user.save()
|
||||
|
||||
// Generate a JWT token for the user.
|
||||
const token = user.generateToken()
|
||||
|
||||
// Convert the database model to a JSON object.
|
||||
const userData = user.toJSON()
|
||||
// console.log('userData: ', userData)
|
||||
|
||||
// Delete the password property.
|
||||
delete userData.password
|
||||
|
||||
return { userData, token }
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in lib/users.js/createUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an array of all user models in the Mongo database.
|
||||
async getAllUsers () {
|
||||
try {
|
||||
// Get all user models. Delete the password property from each model.
|
||||
const users = await this.UserModel.find({}, '-password')
|
||||
|
||||
return users
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/users.js/getAllUsers()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the model for a specific user.
|
||||
async getUser (params) {
|
||||
try {
|
||||
const { id } = params
|
||||
|
||||
const user = await this.UserModel.findById(id, '-password')
|
||||
|
||||
// Throw a 404 error if the user isn't found.
|
||||
if (!user) {
|
||||
const err = new Error('User not found')
|
||||
err.status = 404
|
||||
throw err
|
||||
}
|
||||
|
||||
return user
|
||||
} catch (err) {
|
||||
// console.log('Error in getUser: ', err)
|
||||
|
||||
if (err.status === 404) throw err
|
||||
|
||||
// Return 422 for any other error
|
||||
err.status = 422
|
||||
err.message = 'Unprocessable Entity'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser (existingUser, newData) {
|
||||
try {
|
||||
// console.log('existingUser: ', existingUser)
|
||||
// console.log('newData: ', newData)
|
||||
|
||||
// Input Validation
|
||||
// Optional inputs, but they must be strings if included.
|
||||
if (newData.email && typeof newData.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
if (newData.name && typeof newData.name !== 'string') {
|
||||
throw new Error("Property 'name' must be a string!")
|
||||
}
|
||||
if (newData.password && typeof newData.password !== 'string') {
|
||||
throw new Error("Property 'password' must be a string!")
|
||||
}
|
||||
|
||||
// Save a copy of the original user type.
|
||||
const userType = existingUser.type
|
||||
// console.log('userType: ', userType)
|
||||
|
||||
// If user 'type' property is sent by the client
|
||||
if (newData.type) {
|
||||
if (typeof newData.type !== 'string') {
|
||||
throw new Error("Property 'type' must be a string!")
|
||||
}
|
||||
|
||||
// Unless the calling user is an admin, they can not change the user type.
|
||||
if (userType !== 'admin') {
|
||||
throw new Error("Property 'type' can only be changed by Admin user")
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite any existing data with the new data.
|
||||
Object.assign(existingUser, newData)
|
||||
|
||||
// Save the changes to the database.
|
||||
await existingUser.save()
|
||||
|
||||
// Delete the password property.
|
||||
delete existingUser.password
|
||||
|
||||
return existingUser
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/users.js/updateUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser (user) {
|
||||
try {
|
||||
await user.remove()
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/users.js/deleteUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Used to authenticate a user. If the login and password salt match a user in
|
||||
// the database, then it returns the user model. The Koa REST API uses the
|
||||
// Passport library for this functionality. This function is used to
|
||||
// authenticate users who login via the JSON RPC.
|
||||
async authUser (login, passwd) {
|
||||
try {
|
||||
// console.log('login: ', login)
|
||||
// console.log('passwd: ', passwd)
|
||||
|
||||
const user = await this.UserModel.findOne({ email: login })
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
|
||||
const isMatch = await user.validatePassword(passwd)
|
||||
|
||||
if (!isMatch) {
|
||||
throw new Error('Login credential do not match')
|
||||
}
|
||||
|
||||
return user
|
||||
} catch (err) {
|
||||
// console.error('Error in users.js/authUser()')
|
||||
console.log('')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserLib
|
||||
@@ -0,0 +1,7 @@
|
||||
# Automated End-to-end Tests
|
||||
|
||||
This contains the original boilerplate tests, which are end-to-end tests. These tests are fully automated and test the system directly by making REST API calls with axios.
|
||||
|
||||
These tests function exactly the same as a normal user would, by making real REST API calls to the software. As a result, they are fine for testing internal system components like authorization and user handling. However, they are inappropriate for testing sophisticated endpoints that involve complex operations. For example, interacting with a blockchain, pinging other network systems, or writing data to a secondary database.
|
||||
|
||||
There is some redundancy between these tests and the unit tests. The focus is on *how* the tests are executed. The unit tests call the libraries directly (internally). These e2e tests use the REST API (externally).
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
End-to-end tests for /auth endpoints.
|
||||
|
||||
This test sets up the environment for other e2e tests.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const axios = require('axios').default
|
||||
|
||||
// Local support libraries
|
||||
const config = require('../../../config')
|
||||
const Server = require('../../../bin/server')
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
const AdminLib = require('../../../src/adapters/admin')
|
||||
const adminLib = new AdminLib()
|
||||
|
||||
// const request = supertest.agent(app.listen())
|
||||
const context = {}
|
||||
|
||||
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()
|
||||
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,791 @@
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
const assert = require('chai').assert
|
||||
const config = require('../../../config')
|
||||
const axios = require('axios').default
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
const context = {}
|
||||
|
||||
const UserController = require('../../../src/controllers/rest-api/users/controller')
|
||||
const Adapters = require('../../../src/adapters')
|
||||
const adapters = new Adapters()
|
||||
const UseCases = require('../../../src/use-cases/')
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
// const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('Users', () => {
|
||||
before(async () => {
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
|
||||
// Create a second test user.
|
||||
const userObj = {
|
||||
email: 'test2@test.com',
|
||||
password: 'pass2',
|
||||
name: 'test2'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`)
|
||||
|
||||
context.user2 = testUser.user
|
||||
context.token2 = testUser.token
|
||||
context.id2 = testUser.user._id
|
||||
|
||||
// Get the JWT used to log in as the admin 'system' user.
|
||||
const adminJWT = await testUtils.getAdminJWT()
|
||||
// console.log(`adminJWT: ${admi nJWT}`)
|
||||
context.adminJWT = adminJWT
|
||||
|
||||
// const admin = await testUtils.loginAdminUser()
|
||||
// context.adminJWT = admin.token
|
||||
|
||||
// const admin = await adminLib.loginAdmin()
|
||||
// console.log(`admin: ${JSON.stringify(admin, null, 2)}`)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCases({ adapters })
|
||||
uut = new UserController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /users - Create User', () => {
|
||||
it('should reject signup when data is incomplete', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
email: 'test2@test.com'
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
/* console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
) */
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 422, 'Error code 422 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject signup if no email property is provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
password: 'pass2'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log('err', err)
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'email' must be a string")
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject signup if no password property is provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'test2@test.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'password' must be a string"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject if name property property is not string', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'test322@test.com',
|
||||
password: 'supersecretpassword',
|
||||
name: 1234
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'name' must be a string")
|
||||
}
|
||||
})
|
||||
|
||||
it("should signup of type 'user' by default", async () => {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'test3@test.com',
|
||||
password: 'supersecretpassword',
|
||||
name: 'test3'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
context.user = result.data.user
|
||||
context.token = result.data.token
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test3@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
assert.property(result.data, 'token', 'Token property exists.')
|
||||
assert.equal(result.data.user.type, 'user')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('should not fetch users if the authorization header is missing', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not fetch users if the authorization header is missing the scheme', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: '1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not fetch users if the authorization header has invalid scheme', async () => {
|
||||
const { token } = context
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Unknown ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not fetch users if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should fetch all users', async () => {
|
||||
const { token } = context
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const users = result.data.users
|
||||
// console.log(`users: ${util.inspect(users)}`)
|
||||
|
||||
assert.hasAnyKeys(users[0], ['type', '_id', 'email'])
|
||||
assert.isNumber(users.length)
|
||||
})
|
||||
|
||||
it('should return a 422 http status if biz-logic throws an error', async () => {
|
||||
try {
|
||||
const { token } = context
|
||||
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.useCases.user, 'getAllUsers')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.fail('Unexpected code path!')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.equal(err.response.data, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /users/:id', () => {
|
||||
it('should not fetch user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw 404 if user doesn't exist", async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 404)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 422 for invalid input', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
}
|
||||
})
|
||||
|
||||
it('should fetch own user', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /users/:id', () => {
|
||||
it('should not update user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if non-admin updating other user', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update user type', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'new name',
|
||||
type: 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
// console.log(`Users: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
// assert(result.status === 200, 'Status Code 200 expected.')
|
||||
// assert(result.data.user.type === 'user', 'Type should be unchanged.')
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'type' can only be changed by Admin user"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update other user when not admin', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
name: 'This should not work'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if name property is wrong', async () => {
|
||||
try {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'testToUpdate@test.com',
|
||||
name: {},
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (error) {
|
||||
assert.equal(error.response.status, 422)
|
||||
assert.include(error.response.data, "Property 'name' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if password property is not string', async () => {
|
||||
const { token } = context
|
||||
const _id = context.user._id
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
password: 1234,
|
||||
email: 'test@test.com',
|
||||
name: 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'password' must be a string!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if email is not string', async () => {
|
||||
const { token } = context
|
||||
const _id = context.user._id
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 1234
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update type property if is not string', async () => {
|
||||
try {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
type: 1,
|
||||
email: 'test@test.com',
|
||||
name: 'test',
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'type' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should be able to update other user when admin', async () => {
|
||||
const adminJWT = context.adminJWT
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
name: 'This should work',
|
||||
email: 'test4@test.com',
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
const userName = result.data.user.name
|
||||
assert.equal(userName, 'This should work')
|
||||
})
|
||||
|
||||
it('should update user with minimum inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: { email: 'testToUpdate@test.com' }
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
})
|
||||
|
||||
it('should update user with all inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'testToUpdate@test.com',
|
||||
name: 'my name',
|
||||
username: 'myUsername'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'name')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.name, 'my name')
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
assert.equal(user.username, 'myUsername')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /users/:id', () => {
|
||||
it('should not delete user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if deleting invalid user', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to delete other users unless admin', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should delete own user', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data.success)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
|
||||
it('should be able to delete other users when admin', async () => {
|
||||
const id = context.id2
|
||||
const adminJWT = context.adminJWT
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,178 @@
|
||||
const config = require('../../../config')
|
||||
const axios = require('axios').default
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Mock data
|
||||
// const mockData = require('./mocks/contact-mocks')
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
const ContactController = require('../../../src/controllers/rest-api/contact/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
describe('Contact', () => {
|
||||
beforeEach(() => {
|
||||
uut = new ContactController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /contact/email', () => {
|
||||
it('should throw error if email property is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
formMessage: 'message'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if formMessage property is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'formMessage' must be a string!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if email list provided is not a array', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
emailList: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'emailList' must be a array of emails!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if email list provided is a empty array', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
emailList: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'emailList' must be a array of emails!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email with minimun input', async () => {
|
||||
try {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.contactLib, 'sendEmail').resolves(true)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message'
|
||||
}
|
||||
}
|
||||
}
|
||||
await uut.email(ctx)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email with all inputs', async () => {
|
||||
try {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.contactLib, 'sendEmail').resolves(true)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
emailList: ['email@email.com']
|
||||
}
|
||||
}
|
||||
}
|
||||
await uut.email(ctx)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
const config = require('../../../config')
|
||||
const assert = require('chai').assert
|
||||
|
||||
const axios = require('axios').default
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
const LogsController = require('../../../src/controllers/rest-api/logs/controller')
|
||||
const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('LogsApi', () => {
|
||||
beforeEach(() => {
|
||||
uut = new LogsController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /logs', () => {
|
||||
it('should return false if password is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/logs`,
|
||||
data: {}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
assert.isFalse(result.data.success)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return log', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/logs`,
|
||||
data: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.isTrue(result.data.success)
|
||||
assert.isArray(result.data.data)
|
||||
assert.property(result.data.data[0], 'message')
|
||||
assert.property(result.data.data[0], 'level')
|
||||
assert.property(result.data.data[0], 'timestamp')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return false if files are not found!', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.logsApiLib, 'getLogs').resolves({
|
||||
success: false,
|
||||
data: 'file does not exist'
|
||||
})
|
||||
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.isFalse(ctx.body.success)
|
||||
assert.include(ctx.body.data, 'file does not exist')
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and handle errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.logsApiLib.fs, 'existsSync')
|
||||
.throws(new Error('test error'))
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
ctx.request = {
|
||||
body: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw unhandled error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.logsApiLib.fs, 'existsSync').throws(new Error())
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
ctx.request = {
|
||||
body: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unhandled error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,305 @@
|
||||
const assert = require('chai').assert
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
|
||||
const Validators = require('../../../src/controllers/rest-api/middleware/validators')
|
||||
|
||||
const sinon = require('sinon')
|
||||
const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const context = {}
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('Validators', () => {
|
||||
before(async () => {
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
|
||||
// Create a second test user.
|
||||
const userObj = {
|
||||
email: 'testvalidator@test.com',
|
||||
password: 'pass2',
|
||||
name: 'testvalidator'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
context.id = testUser.user._id
|
||||
|
||||
// Get the JWT used to log in as the admin 'system' user.
|
||||
const adminJWT = await testUtils.getAdminJWT()
|
||||
// console.log(`adminJWT: ${adminJWT}`)
|
||||
context.adminJWT = adminJWT
|
||||
|
||||
// const admin = await testUtils.loginAdminUser()
|
||||
// context.adminJWT = admin.token
|
||||
|
||||
// const admin = await adminLib.loginAdmin()
|
||||
// console.log(`admin: ${JSON.stringify(admin, null, 2)}`)
|
||||
})
|
||||
beforeEach(() => {
|
||||
uut = new Validators()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('ensureUser()', () => {
|
||||
it('should throw 401 if user cant be found', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').resolves(false)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
|
||||
await uut.ensureUser(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token not found', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
await uut.ensureUser(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token is invalid', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await uut.ensureUser(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if user is admin', async () => {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.ensureUser(ctx)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureAdmin()', () => {
|
||||
it('should throw 401 if token not found', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token is invalid', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user cant be found', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').resolves(false)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user is not admin type', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'not admin')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if user is admin', async () => {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.ensureAdmin(ctx)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureTargetUserOrAdmin()', () => {
|
||||
it('should throw 401 if token not found', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token is invalid', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user cant be found', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').resolves(false)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user is not admin type', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: 'Target Id' }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'not admin')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if user is admin', async () => {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
const assert = require('chai').assert
|
||||
|
||||
const Admin = require('../../../src/adapters/admin')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('Admin', () => {
|
||||
beforeEach(() => {
|
||||
uut = new Admin()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Automated E2E tests for the /offer endpoint.
|
||||
*/
|
||||
|
||||
// const config = require('../../../config')
|
||||
// const assert = require('chai').assert
|
||||
|
||||
// const axios = require('axios').default
|
||||
const sinon = require('sinon')
|
||||
|
||||
// const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
// const OfferController = require('../../../src/controllers/rest-api/offer/controller')
|
||||
// const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
let sandbox
|
||||
// let uut
|
||||
describe('OfferApi', () => {
|
||||
beforeEach(() => {
|
||||
// uut = new OfferController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /offer', () => {
|
||||
// it('should pass data to the handlers', async () => {
|
||||
// try {
|
||||
// const mockOffer = {
|
||||
// lokadId: 'SWP',
|
||||
// messageType: 1,
|
||||
// messageClass: 1,
|
||||
// tokenId: 'token-id',
|
||||
// buyOrSell: 'sell',
|
||||
// rateInSats: 1000,
|
||||
// minSatsToExchange: 1250,
|
||||
// signature:
|
||||
// 'H4cRPaAtyNyzG+4Qz+Tp2O7TtFZ7QRsWKKxG71dUZG5xfX0EWRMrBmqM6rH7jToOAT2s9Dm759HMxwP/WTMPzyA=',
|
||||
// sigMsg: 'test',
|
||||
// utxoTxid: 'txid-goes-here',
|
||||
// utxoVout: 1,
|
||||
// offerIpfsId: 'Qmblah',
|
||||
// offerBchAddr:
|
||||
// 'bitcoincash:qzxj37k35z6whp4mj4hrdw2vprx4klcydc6ete5xft',
|
||||
// offerPubKey: 'pubkeyInHex'
|
||||
// }
|
||||
//
|
||||
// const options = {
|
||||
// method: 'post',
|
||||
// url: `${LOCALHOST}/offer`,
|
||||
// data: { offer: mockOffer }
|
||||
// }
|
||||
//
|
||||
// const result = await axios(options)
|
||||
// console.log('result.data: ', result.data)
|
||||
//
|
||||
// // assert.isFalse(result.data.success)
|
||||
// } catch (err) {
|
||||
// assert(false, 'Unexpected result')
|
||||
// }
|
||||
// })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
A manual e2e test for creating an swap offer.
|
||||
|
||||
Ensure the REST API is up an running before running this test.
|
||||
*/
|
||||
|
||||
const axios = require('axios')
|
||||
|
||||
const LOCALHOST = 'http://localhost:5700'
|
||||
|
||||
async function start () {
|
||||
try {
|
||||
const mockOffer = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId:
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 10,
|
||||
numTokens: 0.02
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/offer`,
|
||||
data: { offer: mockOffer }
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
console.log('result.data: ', result.data)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
start()
|
||||
@@ -0,0 +1,8 @@
|
||||
# Unit Tests
|
||||
Unit tests are defined as testing the smallest possible unit of a function. They also do not make any live network calls.
|
||||
|
||||
Unit tests are broken up by directory:
|
||||
|
||||
- [biz-logic](./biz-logic) tests the business logic libraries.
|
||||
- [rest-api](./rest-api) tests the REST API specific handling of the router.
|
||||
- json-rpc (coming soon) tests the JSON-RPC routing using ipfs-coord library.
|
||||
@@ -0,0 +1,153 @@
|
||||
const assert = require('chai').assert
|
||||
|
||||
const BCHJS = require('../../../src/adapters/bch')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const mockData = require('../mocks/bchjs-mock')
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('bch', () => {
|
||||
beforeEach(() => {
|
||||
uut = new BCHJS()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#_verifySignature', () => {
|
||||
it('should return true for valid signature', () => {
|
||||
const offerBchAddr =
|
||||
'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8'
|
||||
const signature =
|
||||
'Hz8bi9CsHaYkk5SGtHLU0aaxFspEXz7IdBNn6xV8ejE6OCuIRoZuVE9QJsGSlJ3Rt0ez2LWD0e292NZ84rRwnfk='
|
||||
const sigMsg = 'example.com'
|
||||
const verifyObj = { offerBchAddr, signature, sigMsg }
|
||||
|
||||
const result = uut._verifySignature(verifyObj)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false for invalid signature', () => {
|
||||
const offerBchAddr =
|
||||
'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8'
|
||||
const signature =
|
||||
'Hz8bi9CsHaYkk5SGtHLU0aaxFspEXz7IdBNn6xV8ejE6OCuIRoZuVE9QJsGSlJ3Rt0ez2LWD0e292NZ84rRwnfd='
|
||||
const sigMsg = 'example.com'
|
||||
const verifyObj = { offerBchAddr, signature, sigMsg }
|
||||
|
||||
const result = uut._verifySignature(verifyObj)
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
|
||||
it('should catch and throw errors', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.BitcoinCash, 'verifyMessage')
|
||||
.throws(new Error('test error'))
|
||||
|
||||
const offerBchAddr =
|
||||
'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8'
|
||||
const signature =
|
||||
'Hz8bi9CsHaYkk5SGtHLU0aaxFspEXz7IdBNn6xV8ejE6OCuIRoZuVE9QJsGSlJ3Rt0ez2LWD0e292NZ84rRwnfk='
|
||||
const sigMsg = 'example.com'
|
||||
const verifyObj = { offerBchAddr, signature, sigMsg }
|
||||
uut._verifySignature(verifyObj)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getPSFTokenBalance', () => {
|
||||
it('should throw error if slpAddress is not provided', async () => {
|
||||
try {
|
||||
await uut.getPSFTokenBalance()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'slpAddress must be a string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return psf tokens balance', async () => {
|
||||
// Mock live network calls.
|
||||
sandbox
|
||||
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
|
||||
.resolves(mockData.psfBalances)
|
||||
|
||||
const slpAddress =
|
||||
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 2)
|
||||
})
|
||||
|
||||
it('should return 0 if the slp address does not has Psf tokens', async () => {
|
||||
// Mock live network calls.
|
||||
sandbox
|
||||
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
|
||||
.resolves(mockData.noPsfBalances)
|
||||
|
||||
const slpAddress =
|
||||
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 0)
|
||||
})
|
||||
|
||||
it('should return 0 for empty balances', async () => {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.bchjs.SLP.Utils, 'balancesForAddress').resolves([])
|
||||
|
||||
const slpAddress =
|
||||
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 0)
|
||||
})
|
||||
})
|
||||
describe('#getPSFTokenBalance', () => {
|
||||
it('should throw error if slpAddr is not provided', async () => {
|
||||
try {
|
||||
await uut.getMerit()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'slpAddr must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if slpAddr provided is invalid type', async () => {
|
||||
try {
|
||||
await uut.getMerit(1)
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'slpAddr must be a string')
|
||||
}
|
||||
})
|
||||
it('should return the merit ', async () => {
|
||||
try {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
|
||||
|
||||
const slpAddr =
|
||||
'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
|
||||
const merit = await uut.getMerit(slpAddr)
|
||||
assert.isNumber(merit)
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user