diff --git a/__tests__/actiontoken-expired.spec.js b/__tests__/actiontoken-expired.spec.js new file mode 100644 index 00000000..a7b0a2df --- /dev/null +++ b/__tests__/actiontoken-expired.spec.js @@ -0,0 +1,90 @@ +require('dotenv').config(); + +const request = require('supertest'); +const { expect } = require('chai'); +const sinon = require('sinon'); +const chai = require('chai'); +const seed = require('./seed'); +const server = require('../server/app'); + +chai.use(require('chai-uuid')); + + +describe( 'Expired ActionToken Transfer ', ()=>{ + let bearerToken; + let bearerTokenB; + let actionToken ; + let clock ; + before(async () => { + + await seed.clear(); + await seed.seed(); + sinon.restore(); + + { + // Authorizes before each of the follow tests + const res = await request(server) + .post('/auth') + .set('treetracker-api-key', seed.apiKey) + .send({ + wallet: seed.wallet.name, + password: seed.wallet.password, + }); + expect(res).to.have.property('statusCode', 200); + bearerToken = res.body.token; + expect(bearerToken).to.match(/\S+/); + } + + { + // Authorizes before each of the follow tests + const res = await request(server) + .post('/auth') + .set('treetracker-api-key', seed.apiKey) + .send({ + wallet: seed.walletB.name, + password: seed.walletB.password, + }); + expect(res).to.have.property('statusCode', 200); + bearerTokenB = res.body.token; + expect(bearerTokenB).to.match(/\S+/); + } + }); + + + beforeEach(() => { + const now = new Date(); // Current date/time + const sevenDaysAgo = new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000); // 7 days ago + clock = sinon.stub(Date, 'now').returns(sevenDaysAgo.getTime()); + + }); + + afterEach(() => { + clock.restore(); + }); + + + it(`Generate access tokens for walletB`, async () => { + await seed.addTokenToWallet(seed.wallet.id); + const res = await request(server) + .get(`/actiontoken/generate?email_id=rohit@gmail.com&limit=5`) + .set('treetracker-api-key', seed.apiKey) + .set('Authorization', `Bearer ${bearerToken}`); + + + expect(res).to.have.property('statusCode', 200); + expect(res.body).to.have.property('actionToken'); + actionToken = res.body.actionToken; + }); + + it('WalletB User should not be able to transfer on expiry', async ()=> { + clock.restore(); + const res = await request(server) + .post(`/actiontoken/transfer`) + .set('treetracker-api-key', seed.apiKey) + .set('Authorization', `Bearer ${bearerTokenB}`) + .send({actionToken}); + + expect(res).to.have.property('statusCode',401); + }) + + }) \ No newline at end of file diff --git a/__tests__/actiontoken-transfer.spec.js b/__tests__/actiontoken-transfer.spec.js new file mode 100644 index 00000000..6cef7a33 --- /dev/null +++ b/__tests__/actiontoken-transfer.spec.js @@ -0,0 +1,112 @@ +/* + * The integration test to test the whole business, with DB + */ +require('dotenv').config(); + +const request = require('supertest'); +const { expect } = require('chai'); +const sinon = require('sinon'); +const chai = require('chai'); +const seed = require('./seed'); +const server = require('../server/app'); + +chai.use(require('chai-uuid')); + +describe('Generate Action Token and Transfer ', () => { + let bearerToken; + let bearerTokenB; + let actionToken ; + + before(async () => { + + await seed.clear(); + await seed.seed(); // this inserts one token to walletA but not walletB among other + sinon.restore(); + + { + // Authorizes before each of the follow tests + const res = await request(server) + .post('/auth') + .set('treetracker-api-key', seed.apiKey) + .send({ + wallet: seed.wallet.name, + password: seed.wallet.password, + }); + expect(res).to.have.property('statusCode', 200); + bearerToken = res.body.token; + expect(bearerToken).to.match(/\S+/); + } + + { + // Authorizes before each of the follow tests + const res = await request(server) + .post('/auth') + .set('treetracker-api-key', seed.apiKey) + .send({ + wallet: seed.walletB.name, + password: seed.walletB.password, + }); + + expect(res).to.have.property('statusCode', 200); + bearerTokenB = res.body.token; + expect(bearerTokenB).to.match(/\S+/); + } + }); + + + + it(`Generate Actiontoken by wallet A`, async () => { + await seed.addTokenToWallet(seed.wallet.id); + const res = await request(server) + .get(`/actiontoken/generate?email_id=rohit@gmail.com&limit=5`) + .set('treetracker-api-key', seed.apiKey) + .set('Authorization', `Bearer ${bearerToken}`); + + + expect(res).to.have.property('statusCode', 200); + expect(res.body).to.have.property('actionToken'); + actionToken = res.body.actionToken; + + }); + + + it(`Transfer actiontoken when B loggedIn`, async () => { + const res = await request(server) + .post(`/actiontoken/transfer`) + .set('treetracker-api-key', seed.apiKey) + .set('Authorization', `Bearer ${bearerTokenB}`) + .send({actionToken }); + + expect(res).to.have.property('statusCode', 200); + expect(res.body).to.have.property('id') + expect(res.body).to.have.property('state','completed') + expect(res.body.parameters.tokens.length).to.equal(2); + + }); + + + + it('Get all transfers Belonging to walletA, should have one and completed', async () => { + const res = await request(server) + .get(`/transfers?wallet=${seed.walletB.name}&limit=10`) + .set('treetracker-api-key', seed.apiKey) + .set('Authorization', `Bearer ${bearerTokenB}`); + + expect(res).to.have.property('statusCode', 200); + expect(res.body.transfers).lengthOf(1); + expect(res.body.transfers[0]).to.have.property('state','completed') + }); + + + it(`walletB, GET /tokens Should have 2 tokens now`, async () => { + const res = await request(server) + .get(`/tokens?limit=10`) + .set('treetracker-api-key', seed.apiKey) + .set('Authorization', `Bearer ${bearerTokenB}`); + + expect(res).to.have.property('statusCode', 200); + expect(res.body.tokens.length).to.equal(2); + }); + +}); + diff --git a/server/handlers/actiontokenHandler/index.js b/server/handlers/actiontokenHandler/index.js new file mode 100644 index 00000000..13331cde --- /dev/null +++ b/server/handlers/actiontokenHandler/index.js @@ -0,0 +1,40 @@ + +const ActionTokenService = require('../../services/ActionTokenService') + +const { actiontokenGenerateSchema , actionTokenTransferSchema } = require('./schemas'); + +const generate = async (req, res) => { + const validatedQuery = await actiontokenGenerateSchema.validateAsync(req.query, { abortEarly: false }); + const { email_id,limit} = validatedQuery; + const { wallet_id } = req + // send payload and this will return us the accessToken + + const actionTokenService = new ActionTokenService(); + const actionToken = await actionTokenService.generate( + email_id, + wallet_id, + limit + ) + + res.json({actionToken}) +}; + + +const transfer = async ( req,res ) => { + const validatedBody = await actionTokenTransferSchema.validateAsync(req.body, { abortEarly: false }); + const { actionToken } = validatedBody + const { wallet_id } = req; + + const actionTokenService = new ActionTokenService(); + + // verfiy actionToken + const tokens = await ActionTokenService.verify(actionToken); + + + const result = await actionTokenService.transferTokens( tokens , wallet_id ) + // then do the transfer using the payload , if possibel + res.status(200).json(result) + +} + +module.exports = {generate,transfer} diff --git a/server/handlers/actiontokenHandler/schemas.js b/server/handlers/actiontokenHandler/schemas.js new file mode 100644 index 00000000..aceb426d --- /dev/null +++ b/server/handlers/actiontokenHandler/schemas.js @@ -0,0 +1,13 @@ +const Joi = require('joi'); + +// Define the schema for validation +const actiontokenGenerateSchema = Joi.object({ + email_id: Joi.string().email().required(), + limit: Joi.number().integer().min(1).max(10).required(), // Adjust the max limit as needed +}); + +const actionTokenTransferSchema = Joi.object({ + actionToken: Joi.string().required() +}); + +module.exports = {actiontokenGenerateSchema,actionTokenTransferSchema} \ No newline at end of file diff --git a/server/routes/actionTokenRouter.js b/server/routes/actionTokenRouter.js new file mode 100644 index 00000000..338c4c5a --- /dev/null +++ b/server/routes/actionTokenRouter.js @@ -0,0 +1,22 @@ +const express = require('express'); + +const router = express.Router(); +const routerWrapper = express.Router(); + +const { + handlerWrapper, + verifyJWTHandler, + apiKeyHandler, +} = require('../utils/utils'); + +const { + generate, + transfer, +} = require('../handlers/actiontokenHandler'); + +router.get('/generate', handlerWrapper(generate)); +router.post('/transfer', handlerWrapper(transfer)); + + +routerWrapper.use('/actiontoken', apiKeyHandler, verifyJWTHandler, router); +module.exports = routerWrapper; diff --git a/server/routes/index.js b/server/routes/index.js index 6e93180c..b80e9312 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -5,4 +5,5 @@ module.exports = [ require('./trustRouter'), require('./walletRouter'), require('./eventRouter'), + require('./actionTokenRouter') ]; diff --git a/server/services/ActionTokenService.js b/server/services/ActionTokenService.js new file mode 100644 index 00000000..3b1a4f3b --- /dev/null +++ b/server/services/ActionTokenService.js @@ -0,0 +1,124 @@ +const JWTTools = require('jsonwebtoken'); +const HttpError = require('../utils/HttpError'); + +const TransferService = require('./TransferService'); +const WalletService = require('./WalletService'); +const TokenService = require('./TokenService'); +const Session = require('../infra/database/Session'); + + + +// PRIVATE and PUBLIC key +const privateKEY = process.env.PRIVATE_KEY.replace(/\\n/g, '\n'); // FS.readFileSync(path.resolve(__dirname, '../../config/jwtRS256.key'), 'utf8'); +const publicKEY = process.env.PUBLIC_KEY.replace(/\\n/g, '\n'); // FS.readFileSync(path.resolve(__dirname, '../../config/jwtRS256.key.pub'), 'utf8'); + +const signingOptions = { + issuer: 'greenstand', + algorithm: 'RS256', +}; + +const verifyOptions = { + issuer: 'greenstand', + expiresIn: '7d', + algorithms: ['RS256'], +}; + +class ActionTokenService { + + constructor() { + this._transferService = new TransferService(); + this._walletService = new WalletService(); + this._tokenService = new TokenService(); + this._session = new Session(); + } + + async generate( email_id,wallet_id,limit) { + const sender_wallet = await this._walletService.getById( + wallet_id + ); + + const tokens = await this._tokenService.getTokens({ + sender_wallet, + limit, + walletLoginId: wallet_id, + }); + // get token ids of all the tokens + const token_ids= tokens.map((token)=> token.id ) + + const now = Math.floor(Date.now() / 1000); // Current time in seconds + const expiration = now + (7 * 24 * 60 * 60); + + const payload = { + sub: email_id, + typ:'send-token', + send_wallet:sender_wallet.name, + token_ids, + token_count:token_ids.length , + exp: expiration, + } + return JWTTools.sign(payload, privateKEY, signingOptions); + }; + + static async verify(token) { + if (!token) { + throw new HttpError(401, 'ERROR: no actionToken supplied '); + } + + try{ + const result = await JWTTools.verify(token, publicKEY, verifyOptions); + + if (result.typ !== 'send-token') { + throw new HttpError(401, 'ERROR: AccessToken, invalid token received'); + } + + return result; + }catch (err) { + if (err.name === 'TokenExpiredError') { + throw new HttpError(401, 'ERROR: ActionToken expired'); + } + throw new HttpError(401, 'ERROR: ActionToken not verified'); + + } +} + +async transferTokens( tokens,wallet_id ){ + try{ + await this._session.beginTransaction(); + const sender_wallet = await this._walletService.getByName( + tokens.send_wallet + ); + const receiver_wallet = await this._walletService.getById( + wallet_id + ); + + const {result,status} = await this._transferService.initiateTransfer( { + tokens:tokens.token_ids, + sender_wallet:sender_wallet.name , + receiver_wallet:receiver_wallet.name + }, + wallet_id, + ); + + if( status !== 202 ){ + throw new HttpError(500,'Error Initialing Transfer') + } + + const finalresult = await this._transferService.fulfillTransfer( + sender_wallet.id, + result.id , + {implicit:true} + ); + + await this._session.commitTransaction(); + return finalresult; + + }catch (e) { + if (this._session.isTransactionInProgress()) { + await this._session.rollbackTransaction(); + } + throw e; + } + } +} + +module.exports = ActionTokenService;