FE-219: support access token (#19)

* FE-219: Removed sample. Seperated provoder and tokenizer parts.

* FE-219: Added webpack. Updated build image tag. Added init with cache.

* FE-219: Added access token support.
This commit is contained in:
Ildar Galeev 2017-03-01 18:39:01 +03:00 committed by GitHub
parent 7a08accac7
commit 3195d8a360
24 changed files with 190 additions and 312 deletions

View File

@ -1,3 +1,5 @@
{
"presets": ["es2015"]
"presets": [
["es2015", { "modules": false }]
]
}

View File

@ -15,7 +15,7 @@ SERVICE_IMAGE_PUSH_TAG ?= $(SERVICE_IMAGE_TAG)
BASE_IMAGE_NAME := service-fe
BASE_IMAGE_TAG := a58a828755e9d342ecbd7071e7dc224ffe546378
BUILD_IMAGE_TAG := 6fb209e428feaa0ef6cec07d3909d8a3c4013537
BUILD_IMAGE_TAG := 629911df323f69e08de05aea7491b0a91cf4722e
CALL_W_CONTAINER := init build clean submodules

View File

@ -1,28 +1,38 @@
# Tokenizer.js
# Tokenizer
[![Build Status](http://ci.rbkmoney.com/buildStatus/icon?job=rbkmoney_private/tokenizer.js/master)](http://ci.rbkmoney.com/job/rbkmoney_private/job/tokenizer.js/job/master)
JavaScript библиотека для токенизации карточных данных клиентов.
## Настройка
Конфигурация происходит в файле [appConfig.json](/src/appConfig.json)
Для изменения конфигурации в рантайме достаточно заменить `appConfig.json`
Например в случае с nginx `appConfig.json` нужно положить в `/usr/share/nginx/html`
Конфигурация происходит в файле [tokenizerConfig.json](/src/tokenizerConfig.json)
Для изменения конфигурации в рантайме достаточно заменить `tokenizerConfig.json`.
Например в случае с nginx `tokenizerConfig.json` нужно положить в `/usr/share/nginx/html`
## Установка
Для загрузки зависимостей выполнить:
Загрузка зивисимостей:
npm install
Сборка библиотеки:
gulp
npm run build
Разработка / запуск примера:
gulp develop
Библиотека будет доступна по адресу: http://localhost:7000/
Пример будет доступен по адресу: http://localhost:7001/
Режим разработки:
npm start
## Использование
Пример использования можно посмотреть [тут](/sample/index.html)
```javascript
Tokenizer.setAccessToken('<invoice access token>');
Tokenizer.card.createToken({
paymentToolType: 'CardData',
cardHolder: '<card holder>',
cardNumber: '<card number>',
expDate: '<exp date>',
cvv: '<cvv>'
}, (token) => {
console.log(token); // { token: 'string', session: 'string' }
}, (error) => {
console.error(error); // { code: 'string', message: 'string' }
});
```

10
config/helpers.js Normal file
View File

@ -0,0 +1,10 @@
const path = require('path');
const _root = path.resolve(__dirname, '..');
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

36
config/webpack.common.js Normal file
View File

@ -0,0 +1,36 @@
const path = require('path');
const helpers = require('./helpers');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry: {
tokenizer: './src/tokenizer.js',
tokenizerProvider: './src/tokenizerProvider.js'
},
resolve: {
modules: [
path.join(__dirname, 'src'),
'node_modules'
]
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader'],
}
]
},
plugins: [
new CopyWebpackPlugin([{
from: './src/rpc/provider.html'
}, {
from: './src/tokenizerConfig.json'
}])
],
output: {
path: helpers.root('dist'),
filename: '[name].js'
},
};

9
config/webpack.dev.js Normal file
View File

@ -0,0 +1,9 @@
const webpackMerge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
module.exports = webpackMerge(commonConfig, {
devtool: 'eval-source-map',
devServer: {
stats: 'minimal'
}
});

26
config/webpack.prod.js Normal file
View File

@ -0,0 +1,26 @@
const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
module.exports = webpackMerge(commonConfig, {
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true,
}
})
]
});

View File

@ -1,76 +0,0 @@
import gulp from 'gulp';
import browserify from 'browserify';
import babelify from 'babelify';
import source from 'vinyl-source-stream';
import uglify from 'gulp-uglify';
import rename from 'gulp-rename';
import connect from 'gulp-connect';
import eslint from 'gulp-eslint';
import karma from 'karma';
import cors from 'cors';
const config = {
dist: 'dist'
};
gulp.task('lint', () => {
return gulp.src('src/**/*.js')
.pipe(eslint())
.pipe(eslint.format());
});
gulp.task('browserify', ['lint'], () => {
return browserify({
entries: 'src/bootstrap.js',
extensions: ['.js'],
debug: true
}).transform('babelify').bundle()
.pipe(source('tokenizer.js'))
.pipe(gulp.dest(config.dist));
});
gulp.task('uglify', ['browserify'], () => {
return gulp.src(`${config.dist}/tokenizer.js`)
.pipe(rename('tokenizer.min.js'))
.pipe(uglify())
.pipe(gulp.dest(config.dist));
});
gulp.task('copyStatic', () => {
return gulp.src(['src/rpc/provider.html', 'src/tokenizerConfig.json'])
.pipe(gulp.dest(config.dist));
});
gulp.task('test', done => {
const KarmaServer = karma.Server;
new KarmaServer({
configFile: `${__dirname}/karma.conf.js`
}, done).start();
});
gulp.task('connectDist', () => {
connect.server({
root: 'dist',
middleware: function() {
return [cors()];
},
host: '127.0.0.1',
port: 7000
});
});
gulp.task('connectSample', () => {
connect.server({
root: 'sample',
host: '127.0.0.1',
port: 7001
});
});
gulp.task('watch', () => {
gulp.watch('src/**/*', ['build', 'copyStatic']);
});
gulp.task('build', ['uglify', 'copyStatic']);
gulp.task('develop', ['build', 'connectDist', 'watch', 'connectSample']);
gulp.task('default', ['build']);

View File

@ -1,17 +0,0 @@
module.exports = function (config) {
config.set({
frameworks: ['mocha', 'browserify'],
browsers: ['Chrome'],
files: [
'tests/**/*.test.js'
],
preprocessors: {
'tests/**/*.test.js': [ 'browserify' ]
},
reporters: ['mocha'],
browserify: {
debug: true,
transform: [ 'babelify' ]
}
});
};

View File

@ -1,36 +1,30 @@
{
"name": "tokenizer.js",
"name": "Tokenizer",
"version": "1.0.0",
"description": "",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/rbkmoney/tokenizer.js.git"
},
"author": "rbkmoney",
"licenses": [],
"scripts": {
"build": "gulp build"
"start": "webpack-dev-server --config config/webpack.dev.js --inline --progress --port 7000",
"build": "rimraf dist && webpack --config config/webpack.prod.js"
},
"devDependencies": {
"babel-core": "^6.23.1",
"babel-loader": "^6.3.2",
"babel-preset-es2015": "^6.9.0",
"babel-register": "^6.11.6",
"babelify": "^7.3.0",
"browserify": "^13.0.1",
"chai": "^3.5.0",
"cors": "^2.8.1",
"gulp": "^3.9.1",
"gulp-connect": "^5.0.0",
"gulp-eslint": "^3.0.1",
"gulp-rename": "^1.2.2",
"gulp-uglify": "^1.5.4",
"karma": "^1.2.0",
"karma-browserify": "^5.1.0",
"karma-chrome-launcher": "^1.0.1",
"karma-mocha": "^1.1.1",
"karma-mocha-reporter": "^2.1.0",
"karma-phantomjs-launcher": "^1.0.1",
"mocha": "^3.0.2",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.7.0"
"copy-webpack-plugin": "^4.0.1",
"eslint": "^3.16.1",
"eslint-loader": "^1.6.3",
"path": "^0.12.7",
"rimraf": "^2.6.1",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.4.1",
"webpack-merge": "^3.0.0"
},
"dependencies": {
"fingerprintjs2": "^1.4.1",

View File

@ -1,66 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tokenization sample</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.1.0.slim.min.js"
integrity="sha256-cRpWjoSOw5KcyIOaZNo4i6fZ9tKPhYYb6i5T9RSVJG8=" crossorigin="anonymous"></script>
<script type="application/javascript" src="http://localhost:7000/tokenizer/tokenizer.js"></script>
<script>
$(function () {
$('#request-result').hide();
$('#pay-button').click(function () {
Tokenizer.setPublicKey('eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJUdEZzelc3NDB2NTQ1MThVUVg1MGNnczN1U2pCSXkxbDdGcDVyMHdmYzFrIn0.eyJqdGkiOiJjODBhZjlmYi0yNzY5LTQ2YWItOTg4NC0wZWQ0YTVmMjRiOTYiLCJleHAiOjAsIm5iZiI6MCwiaWF0IjoxNDc1MTMxNjAwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMxMjQ1L2F1dGgvcmVhbG1zL2V4dGVybmFsIiwiYXVkIjoidG9rZW5pemVyIiwic3ViIjoiNGQxMmQyMTMtZWU1ZS00ZWEzLTg2YTYtMDc5ZjZkNDM3NWExIiwidHlwIjoiT2ZmbGluZSIsImF6cCI6InRva2VuaXplciIsIm5vbmNlIjoiMGUzYTU2ZmQtZGUxNy00NTI1LTgxNmYtNjM1YTEzNWJlYTYwIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiNDU3MWE0NjQtYmRjYi00YTkyLTg2ZjQtYWI5YjExYzgyNjE5IiwiY2xpZW50X3Nlc3Npb24iOiI0OTU1MWE3ZC04NGNiLTQxZTUtOTQ2OC0wZmQ3ZDg5ZGYxZmUiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiXX0sInJlc291cmNlX2FjY2VzcyI6eyJjb21tb24tYXBpIjp7InJvbGVzIjpbInBheW1lbnRfdG9vbF90b2tlbnM6Y3JlYXRlIl19fX0.ZayOtPaUNxDfBZh_t8IfTUNOJ8653v2lkNX3pDBEyQV28EGf8qbIFJRtA7LbGwdd8brhRIWLk2XBco7RZUX_GYVHIlRo0IvGAzyLYozjXWdfZHaTkChCpk6QnTCTgYeFxTMbgtYYBXOS7oT0tmQZY-N3O0cuIeBItGU8lzaNqfwH9i61WETZKkHYQ_wL28Kbip9IDSgxqDUWnohDA3ee5QROiw-J0DU8MmEkKBSC4owPkYQTFCJr4A69_3hsXArSh3xYu6gkbHoS3CWXMPMtbpFrSrZG191aRwV9ZQIouzd5jKsk6IRiPVhAWSWayd44qRYTgugMX3Tz06O1hOXkDg');
Tokenizer.card.createToken({
"paymentToolType": "cardData",
"cardHolder": $('#card-holder').val(),
"cardNumber": $('#card-number').val(),
"expDate": $('#exp-date').val(),
"cvv": $('#cvv').val()
}, function (result) {
$('#request-result').removeClass('alert-danger').addClass('alert-success').show().text(JSON.stringify(result));
}, function (error) {
$('#request-result').removeClass('alert-success').addClass('alert-danger').show().text(JSON.stringify(error.message));
});
});
});
</script>
</head>
<body>
<div class="container" style="padding-top: 150px">
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<form>
<div class="form-group">
<label>Card holder</label>
<input id="card-holder" class="form-control" placeholder="card holder" value="Test Holder">
</div>
<div class="form-group">
<label>Card number</label>
<input id="card-number" class="form-control" placeholder="card number" value="4242424242424242">
</div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Exp date</label>
<input id="exp-date" class="form-control" placeholder="exp date" value="03/17">
</div>
<div class="col-md-6">
<label>CVV</label>
<input id="cvv" class="form-control" placeholder="cvv" value="638">
</div>
</div>
</div>
<button id="pay-button" type="button" class="btn btn-success">Pay</button>
</form>
<div class="alert" style="margin-top: 30px" id="request-result"></div>
</div>
</div>
</div>
</body>
</html>

36
src/bootstrap.js vendored
View File

@ -1,36 +0,0 @@
import 'whatwg-fetch';
import RpcProvider from './rpc/RpcProvider';
import RpcConsumer from './rpc/RpcConsumer';
import ClientInfo from './clientInfo/ClientInfo';
import Utils from './utils/Utils';
import includes from './polyfills/includes';
(function init() {
includes();
const host = Utils.getScriptUrl();
if (host.includes(this.location.host)) {
new RpcProvider();
} else {
const clientInfo = new ClientInfo();
const rpc = new RpcConsumer(host);
let publicKey;
this.Tokenizer = {
setPublicKey: key => (publicKey = key),
card: {
createToken: (cardData, success, error) => {
if (publicKey) {
const request = {
paymentTool: cardData,
clientInfo: clientInfo.getInfo()
};
rpc.createToken(publicKey, request, success, error);
} else {
error({
message: 'Public key required'
});
}
}
}
};
}
}).call(window || {});

View File

@ -1,12 +1,9 @@
import 'whatwg-fetch';
import Utils from '../utils/Utils';
export default class ConfigLoader {
static load() {
const scriptUrl = Utils.getScriptUrl();
const appConfigUrl = Utils.getOrigin(scriptUrl);
return new Promise((resolve, reject) => {
fetch(`${appConfigUrl}/tokenizerConfig.json`, {
fetch('tokenizerConfig.json', {
method: 'GET',
headers: {
'Content-Type': 'application/json'

View File

@ -1,7 +0,0 @@
export default function () {
if (!String.prototype.includes) {
String.prototype.includes = function () {
return String.prototype.indexOf.apply(this, arguments) !== -1;
};
}
}

View File

@ -1,12 +1,10 @@
/* global easyXDM:true */
import 'madlib-shim-easyxdm';
import Utils from '../utils/Utils';
export default class {
constructor(host) {
const url = Utils.getOrigin(host);
constructor(providerEndpoint) {
return new easyXDM.Rpc({
remote: `${url}/provider.html`
remote: `${providerEndpoint}/provider.html`
}, {
remote: {
createToken: {}

View File

@ -5,6 +5,6 @@
<title>Provider</title>
</head>
<body>
<script type="application/javascript" src="tokenizer.min.js"></script>
<script type="application/javascript" src="tokenizerProvider.js"></script>
</body>
</html>

25
src/tokenizer.js Normal file
View File

@ -0,0 +1,25 @@
import RpcConsumer from './rpc/RpcConsumer';
import ClientInfo from './clientInfo/ClientInfo';
import getOrigin from './utils/getOrigin';
(function init() {
const clientInfo = new ClientInfo();
const consumer = new RpcConsumer(getOrigin());
let accessToken;
this.Tokenizer = {
setAccessToken: token => (accessToken = token),
card: {
createToken: (cardData, success, error) => {
if (accessToken) {
const request = {
paymentTool: cardData,
clientInfo: clientInfo.getInfo()
};
consumer.createToken(accessToken, request, success, error);
} else {
error({ message: 'Access token required' });
}
}
}
};
}).call(window || {});

View File

@ -1,4 +1,3 @@
{
"capiUrl": "http://localhost:7051",
"keycloakUrl": "http://localhost:8080/auth"
"capiEndpoint": "https://api.rbk.test:8080"
}

5
src/tokenizerProvider.js Normal file
View File

@ -0,0 +1,5 @@
import RpcProvider from './rpc/RpcProvider';
(function init() {
new RpcProvider();
}).call(window || {});

View File

@ -1,44 +1,25 @@
import 'whatwg-fetch';
import ConfigLoader from '../loaders/ConfigLoader';
import generateGuid from '../utils/generateGuid';
export default class CardTokenizer {
static createToken(key, cardData, success, error) {
static createToken(accessToken, cardData, success, error) {
ConfigLoader.load().then(config => {
CardTokenizer.refreshToken(key, config.keycloakUrl)
.then(keycloakRes => CardTokenizer.tokenize(keycloakRes.access_token, cardData, config.capiUrl)
.then(capiRes => success(capiRes))
.catch(cause => error(cause)))
.catch(cause => error(cause))
CardTokenizer.tokenize(accessToken, cardData, config.capiEndpoint)
.then(capiRes => success(capiRes))
.catch(cause => error(cause));
});
}
static refreshToken(key, keycloakUrl) {
static tokenize(accessToken, cardData, capiEndpoint) {
return new Promise((resolve, reject) => {
fetch(`${keycloakUrl}/realms/external/protocol/openid-connect/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `grant_type=refresh_token&client_id=tokenizer&refresh_token=${key}`
}).then(response => {
if (response.status >= 200 && response.status < 300) {
resolve(response.json());
} else {
reject(response);
}
}).catch(() => reject('Error request to keycloak'));
});
}
static tokenize(token, cardData, capiUrl) {
return new Promise((resolve, reject) => {
fetch(`${capiUrl}/processing/payment_tools`, {
fetch(`${capiEndpoint}/v1/processing/payment_tools`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8',
'X-Request-ID': this.guid(),
'Authorization': `Bearer ${token}`,
'X-Request-ID': generateGuid(),
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify(cardData)
}).then(response => {
@ -47,14 +28,7 @@ export default class CardTokenizer {
} else {
reject(response);
}
}).catch(() => reject('Error request to CAPI'));
}).catch(() => reject('Error request to api'));
});
}
static guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return `${s4()}${s4()}-${s4()}-${s4()}`;
}
}

View File

@ -1,13 +0,0 @@
export default class Utils {
static getScriptUrl() {
const scripts = document.getElementsByTagName('script');
const element = scripts[scripts.length - 1];
return element.src;
}
static getOrigin(url) {
const parser = document.createElement('a');
parser.href = url;
return parser.origin;
}
}

View File

@ -0,0 +1,9 @@
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
function guid() {
return `${s4()}${s4()}-${s4()}-${s4()}`;
}
export default guid;

13
src/utils/getOrigin.js Normal file
View File

@ -0,0 +1,13 @@
function getScriptUrl() {
const scripts = document.getElementsByTagName('script');
const element = scripts[scripts.length - 1];
return element.src;
}
function getOrigin() {
const parser = document.createElement('a');
parser.href = getScriptUrl();
return parser.origin;
}
export default getOrigin;

View File

@ -1,14 +0,0 @@
import chai from 'chai';
import ClientInfo from '../src/clientInfo/ClientInfo'
chai.should();
describe('Client info', () => {
const clientInfo = new ClientInfo();
describe('getInfo', () => {
it('fingerprint should match regexp', () => {
const info = clientInfo.getInfo();
info.fingerprint.should.match(/^[0-9a-f]{32}$/i);
});
});
});