Part 1: Create a Secure Steemconnect-Powered STEEM/SBD Payment Portal with React and AdonisJS

@creatrixity · 2018-08-23 15:38 · utopian-io

Repository

What Will I Learn?

This is a two part series in which we build a payment portal that uses the Steemconnect API to accept STEEM/SBD payments directly. As we progress while building this portal, we will learn about and use the techniques below to achieve our goal.

  • Test Driven Development approach to building scalable applications
  • Dynamically generating payment endpoints using URL encoding.
  • Setting up the server-side API payment to process payment requests.
  • Securing payments from fraud by leveraging custom generated security tokens.

Requirements

Difficulty

  • Advanced

Tutorial Repository

Tutorial Contents

  • Introduction.
  • Setting up AdonisJS Installation
  • Writing Feature Tests for Wallets
  • Setting up user authentication.

In this installment, we will be strictly working with AdonisJs and the server . We will setup wallets for our users to record their transactions. We will also create user authentication systems with Adonis Persona. We will then visit security token management. We will also be writing our code in a test driven development fashion as we'd like to assure ourselves that we are not recording any false positives.

Introduction.

Disclaimer:

This tutorial is not the ideal introduction to AdonisJS or React for beginners. I'd strongly advise you have a grasp of object oriented programming and you are fairly comfortable with asynchronous programming.

Also, if you are not familiar with functional testing in AdonisJS, I wrote a very helpful article to get you started.

I'd be overjoyed if you took a trip to see these resources before we proceed:

Finally, every line of code in this tutorial is available on Github

Briefing.

We covered our scope above. So let's get to it. We'll be calling our app Paysy.

Setting Up the AdonisJS Installation

I'm assuming your development machine runs the Linux operating system. Windows users will be right at home too. I'm also assuming you have the Node.js runtime and NPM installed.

To install AdonisJS on your machine, we first have to get the global command line interface (CLI). We can install that by running:

npm i -g @adonisjs/cli

Once the installation completes, make sure that you can run adonis from your command line.

adonis --help

Next, we need to create an app called paysy from the CLI. We're interested in the API and web functionalities so we pass the --fullstack additional flag.

adonis new paysy --fullstack

You should see an output similar to the one below.

Paysy Installation

Also, let's add the sqlite3 and mysql dependencies. We'll have sqlite3 for our testing database and MySQL for the production database.

npm install mysql sqlite3 --save-dev

Let's change directories to the paysy directory and start the development server.

cd paysy && adonis serve --dev

We receive a tidy little JSON response if we head over to http://127.0.0.1:3333

{"greeting":"Hello world in JSON"}

Setting Up Application Config

We need to configure environmental variables. Let's update the contents of our .env file to the content below. Leave the rest of the parameters untouched.

HOST=127.0.0.1
PORT=3333
APP_URL=http://${HOST}:${PORT}
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=paysy

We'll also set our authentication method to jwt since we'll be using that method. Edit config/auth.js and set authenticator to jwt

  /*
  |--------------------------------------------------------------------------
  | Authenticator
  |--------------------------------------------------------------------------
  |
  | Authentication is a combination of serializer and scheme with extra
  | config to define on how to authenticate a user.
  |
  | Available Schemes - basic, session, jwt, api
  | Available Serializers - lucid, database
  |
  */
  authenticator: "jwt",

Setting Up Testing Environment

AdonisJS relies on the @adonisjs/vow package as its core testing framework. We may install it by running

adonis install @adonisjs/vow

We should get a screen like below

Adonis Vow screen

We now need to register its service provider in the aceProviders array available at start/app.js .

    const aceProviders = [
      '@adonisjs/vow/providers/VowProvider'
    ]

We must also define a couple of behaviors for our tests. We can define these behaviours in a vowfile.js script available at the project root. We'd like Adonis to spin up a server and run migrations before any tests are run. Then, we'd like Adonis to destroy the server and reset the migrations after we are done testing.

'use strict'

/*
!|--------------------------------------------------------------------------
| Vow file
|--------------------------------------------------------------------------
|
| The vow file is loaded before running your tests. This is the best place
| to hook operations `before` and `after` running the tests.
|
*/

const ace = require('@adonisjs/ace')

module.exports = (cli, runner) => {
  runner.before(async () => {
    /*
    |--------------------------------------------------------------------------
    | Start the server
    |--------------------------------------------------------------------------
    |
    | Starts the http server before running the tests. You can comment this
    | line, if http server is not required
    |
    */
    use('Adonis/Src/Server').listen(process.env.HOST, process.env.PORT)

    /*
    |--------------------------------------------------------------------------
    | Run migrations
    |--------------------------------------------------------------------------
    |
    | Migrate the database before starting the tests.
    |
    */
    await ace.call('migration:run')
  })

  runner.after(async () => {
    /*
    |--------------------------------------------------------------------------
    | Shutdown server
    |--------------------------------------------------------------------------
    |
    | Shutdown the HTTP server when all tests have been executed.
    |
    */
    use('Adonis/Src/Server').getInstance().close()

    /*
    |--------------------------------------------------------------------------
    | Rollback migrations
    |--------------------------------------------------------------------------
    |
    | Once all tests have been completed, we should reset the database to it's
    | original state
    |
    */
    await ace.call('migration:reset')
  })
}

Run adonis test in your terminal now and you'll hopefully get the below result.

Initial test shot

Building the Application

We've successfully setup our installation and we must proceed with building our application now. We'll be following a series of steps to help us achieve our purpose.

Obtaining Requirements

Our app must do the following:

  1. Successfully register and authenticate new users.
  2. Quickly generate security token nonces we'll use in verifying transaction legitimacy.
  3. Promptly update the appropriate user wallets with the new balances whenever funding is successful.

Setting Up Our Database Tables

Based on the requirements we have above, we would need a wallets table to contain all the STEEM and SBD data we'll be storing. AdonisJS can help us create the required table(s) via migrations.

We'll be generating the wallet migrations through the CLI.

adonis make:migration wallet

A little dialog should come up on the CLI asking if we'd like to create or select a table. We'll go with the create table option. Open up the newly generated migration available at database/migrations and let's add some code. We'll be checking to see if this table is yet to be created before proceeding. We'll also need the name, balance and user_id fields to store important information.

'use strict'

const Schema = use('Schema')

class WalletSchema extends Schema {
  up () {
    if (await this.hasTable('wallets')) return;

    this.create('wallets', (table) => {
      table.increments()
      table.integer("user_id");
      table.string("name");
      table.float("balance", 8, 3);
      table.timestamps()
    })
  }

  down () {
    this.drop('wallets')
  }
}

module.exports = WalletSchema

We'll run our migration now. This will generate the wallets table.

adonis migration:run

We've successfully set up our database. Let's create a quick AdonisJS Lucid ORM model for our wallets table. Create Wallet.js in app/Models and add some code

'use strict'

const Model = use('Model')

class Wallet extends Model {
    user () {
        return this.belongsTo('App/Models/User')
    }
}

module.exports = Wallet

Writing Feature Tests for Wallets

For our wallets, we'd like to be able to do the following:

  1. We'd like to be able to add a new wallet entry through the HTTP client.

  2. We'd like to be able to retrieve wallet information through the HTTP client.

  3. We'd also like to be able to update wallet information. This way, we'd be able to update the balance whenever a user funds STEEM or SBD.

  4. We also would like to be able to list and filter homes according to criteria.

We'd also like to extract the logic into lots of separate classes that can be reused in other parts of our app.

Creating Our Wallet Test Suite

AdonisJS comes fully equipped with test capabilities. We can generate a test through the CLI for our Wallet suite of tests. Make sure you choose to generate a functional test as we'll be attempting to test HTTP routes.

adonis make:test Wallet

wallet.spec.js should be available at the test/functional directory now.

___Writing Our First Wallet Test__

Open up test/functional/wallet.spec.js and we are greeted with a default test that looks like this:

    'use strict'

    const { test } = use('Test/Suite')('Home')

    test('make sure 2 + 2 is 4', async ({ assert }) => {
      assert.equal(2 + 2, 4)
    })

Not bad, but we'd love to have a real functional test. Let's replace the content with some new content. We'll import the test and trait methods as we'll need them. The Test/ApiClient trait allows us to make HTTP calls to the backend.

'use strict'

const { test, trait } = use('Test/Suite')('Wallet')

trait('Test/ApiClient')

Next, we add our first test. We attempt to create a wallet by posting data to the /api/v1/wallets route. We then proceed to make sure the wallet was really created by querying the wallets endpoint with the wallet's id as the parameter. We then get assertions by measuring JSON responses from both operations.

test('Should create a wallet through the HTTP client', async ({ client }) => {
  let data = {
    name: 'STEEM',
    user_id: 1,
    balance: 0.000
  }

  const wallet = await client
                      .post(`/api/v1/wallets`)
                      .send(data)
                      .end()

  const response = await client.get(`/api/v1/wallets/${wallet.body.id}`).end()

  response.assertStatus(200)

  response.assertJSONSubset(data);

}).timeout(0)

We run the test and sure enough we get a red failing test. Let's write the implementation to get our tests passing.

Failing test

__Passing Our Wallet Tests___

We'll hop into the terminal and run a command to generate our Wallets controller

adonis make:controller Wallets

Let's write code that passes our test. We'll edit the first route in the start/routes.js and write some code. We'll create a whole lot of routes easily by using the route.factory.

Route.resource("/api/v1/wallets", "WalletsController");

Let's add some code to the Wallet class. We'll import the Wallet model.

const Wallet = use("App/Models/Wallet");

class WalletsController {}

We'll create the store method now. Within it, we'll be creating the wallet. We'll be using the ES6 Object Spread proposal to set some block scoped variables. We're retrieving values from request.all()

  async store({ request }) {
    let { name, user_id, balance } = request.all();
  }

We now need to create a new Wallet (if none matching the provided data exists) using the data received. We then return the created Wallet instance in JSON format.

  let wallet = await Wallet.findOrCreate({
    name,
    user_id
    balance
  })

  return wallet.toJSON()

We also would like to show the created wallet on its own special endpoint. For this, we will add the show method and we'll just grab the id of the wallet needed from the URL using the destructured params object. We'll then fetch it and return it in JSON format.

  async show({ request, params }) {
    let { id } = params;

    let wallet = await Wallet.find(id);

    return wallet.toJSON();
  }

Last of all we need to make sure our wallet can be updated through the HTTP client. We'll add another test that should update the wallet with an id of 1. We'll simply fire a PUT request to our endpoint and run assertions on the JSON returned and the status code of the response.

test("Should update the wallet with the id #1 through the HTTP client", async ({
  client
}) => {
  let walletID = 1;

  let data = {
    balance: 5.0
  };

  const wallet = await client
    .put(`/api/v1/wallets/${walletID}`)
    .send(data)
    .end();

  const response = await client.get(`/api/v1/wallets/${walletID}`).end();

  response.assertStatus(200);

  response.assertJSONSubset(data);
}).timeout(0);

We run adonis test and sure enough our test fails. Let's get it passing. We'll add the update method to our wallet controller. Within this method we will simply find and update the wallet with new data.

  async update({ request, params }) {
    let { id } = params;
    let { balance } = request.all();
    let data = {
        balance 
    }

    let wallet = await Wallet.query()
      .where("id", id)
      .update(data);

    return wallet.toJSON();
  }

Let's save, jump back into the terminal and run our test

adonis test

Congratulations, our tests turn green! We have completed the first phase of TDD for our wallet.

Passing test

Refactoring for Cleaner, Reusable Code.

We'll get our code cleaner and better reusable by extracting functionality into a WalletManager.js class. Create App/Managers/WalletManager.js and we'll move some content from our WalletController.js class to our new one. We're not adding new code here, simply reusing the code we already have. We extract core functionality into three methods:

  • findOrCreateWallet
  • updateWalletByID
  • findWalletByID
"use strict";

const Wallet = use("App/Models/Wallet");

class WalletManager {
  static async findOrCreateWallet(payload) {
    let wallet = await Wallet.findOrCreate(payload.data);

    return wallet.toJSON();
  }

  static async updateWalletByID(payload) {
    let wallet = await Wallet.query()
      .where("id", payload.id)
      .update(payload.data);

    return wallet.toJSON();
  }

  static async findWalletByID(payload) {
    let wallet = await Wallet.find(payload.id);
    return wallet.toJSON();
  }
}

module.exports = WalletManager;

Our WalletController should look like the below now. It's much more skinnier now that we have moved the core functionality to a reusable class.

"use strict";

const WalletManager = use("App/Managers/WalletManager");

class WalletsController {
  async store({ request }) {
    let { name, user_id, balance } = request.all();

    return WalletManager.findOrCreateWallet({
      data: {
        name,
        user_id,
        balance
      }
    });
  }

  async show({ request, params }) {
    let { id } = params;

    return WalletManager.findWalletByID({ id });
  }

  async update({ request, params }) {
    let { id } = params;
    let data = request.all();

    return WalletManager.updateWalletByID({ id, data });
  }
}

module.exports = WalletsController;

We run our tests again and nothing breaks so we can move on.

Adding Users to our App.

A payment server is no good without any actual users. We'll add users to our application and we'll use adonis-persona to speed up this process. Run this to install

adonis install @adonisjs/persona

Follow up by registering the provider inside the providers array in start/app.js:

const providers = [
  '@adonisjs/persona/providers/PersonaProvider'
]

Since Persona does not come with any implementations, we must create one. We'll generate a UserController class.

adonis make:controller User

Next we update our start/routes.js class and add a route factory for our UserController

Route.resource("/api/v1/users", "UserController");

We'll write a test in advance (cause that's the cool thing to do). First of all, we'll generate a functional test suite for the user class.

adonis make:test user

We'll then add the below test to it.

test("Should create a user through the HTTP client", async ({ client }) => {
  let data = {
    email: "john.doe@example.com",
    password: "secret",
    password_confirmation: "secret"
  };

  const user = await client
    .post(`/api/v1/users`)
    .send(data)
    .end();

  const response = await client.get(`/api/v1/users/${user.body.id}`).end();

  response.assertStatus(200);

  response.assertJSONSubset(data);
}).timeout(0);

We get our expected failing test. Now, let's get it green. We'll add the index, store and show methods to the UserController class. Our index method shows us all our available users. We'll keep our core functionality in the UserManager class we'll soon create.

"use strict";

const UserManager = use("App/Managers/UserManager");

class UserController {
  async index() {
    return await UserManager.all();
  }

  async store({ request, auth }) {
    const data = request.only(["email", "password", "password_confirmation"]);

    try {
      const user = await UserManager.createUserFromData({ data });

      await auth.login(user);

      return user;
    } catch (e) {
      return e;
    }
  }

  async show({ params }) {
    const { id } = params;

    return UserManager.findUserByID({ id });
  }
}

module.exports = UserController;

Lets create App/Managers/UserManager.js and then we'll define the methods required on it.

Firstly, the all method returns all our users. We use the Persona package to register users in the createUserFromData method. We use the findUserByID to simply return any user matching the id provided.

```js "use str

#utopian-io #tutorial #programming #development #steem
Payout: 0.000 HBD
Votes: 202
More interactions (upvote, reblog, reply) coming soon.