Skip to content

Commit

Permalink
Deploy full oauth functionality (#99)
Browse files Browse the repository at this point in the history
* Fix encodeURI params and some clean up (#98) (#4)

* v2.7.7

* fix encodeURI issue

* version bump

Co-authored-by: Ajaykumar <[email protected]>

* Add OAUTH endpoint to constants

* Add full oauth support

* Add oauth endpoint to expected properties

* Rename access_token -> appAccessToken

* Remove oauth functions from utils

* Add user token demo

* Fix missing context var

* Add missing function description

* Add urldecode declaration to docstring

* Remove trailling spaces

* Migrate credential functions to credentials.js

Co-authored-by: Ajaykumar <[email protected]>
  • Loading branch information
TotallyNotChase and pajaydev authored Jun 10, 2020
1 parent ffa4bf3 commit d389d49
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 43 deletions.
32 changes: 32 additions & 0 deletions demo/getUserToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const readline = require('readline');
const Ebay = require('../src/index');
const { clientId, clientSecret, redirectUri } = require('./credentials/index');

let ebay = new Ebay({
clientID: clientId,
clientSecret: clientSecret,
redirectUri: redirectUri,
body: {
grant_type: 'authorization_code',
scope: 'https://api.ebay.com/oauth/api_scope'
}
});

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const authURL = ebay.getUserAuthorizationUrl();
console.log(`Please go here for auth code: ${authURL}`);
rl.question("Enter the auth code recieved from the redirect url (should urldecode it first): ", code => {
rl.close();
ebay.getUserTokenByCode(code).then(data => {
console.log('User token by code response:-');
console.log(data);
ebay.getUserTokenByRefresh().then(data => {
console.log('User token by refresh token response:-');
console.log(data);
});
})
});
20 changes: 10 additions & 10 deletions src/buy-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const { encodeURLQuery, base64Encode } = require('./common-utils');

const getItem = function (itemId) {
if (!itemId) throw new Error('Item Id is required');
if (!this.options.access_token) throw new Error('Missing Access token, Generate access token');
const auth = 'Bearer ' + this.options.access_token;
if (!this.options.appAccessToken) throw new Error('Missing Access token, Generate access token');
const auth = 'Bearer ' + this.options.appAccessToken;
const id = encodeURIComponent(itemId);
this.options.contentType = 'application/json';
return makeRequest(this.options, `/buy/browse/v1/item/${id}`, 'GET', auth).then((result) => {
Expand All @@ -17,9 +17,9 @@ const getItem = function (itemId) {

const getItemByLegacyId = function (legacyOptions) {
if (!legacyOptions) throw new Error('Error Required input to get Items By LegacyID');
if (!this.options.access_token) throw new Error('Missing Access token, Generate access token');
if (!this.options.appAccessToken) throw new Error('Missing Access token, Generate access token');
if (!legacyOptions.legacyItemId) throw new Error('Error Legacy Item Id is required');
const auth = 'Bearer ' + this.options.access_token;
const auth = 'Bearer ' + this.options.appAccessToken;
let param = 'legacy_item_id=' + legacyOptions.legacyItemId;
param += legacyOptions.legacyVariationSku ? '&legacy_variation_sku=' + legacyOptions.legacyVariationSku : '';
this.options.contentType = 'application/json';
Expand All @@ -35,8 +35,8 @@ const getItemByLegacyId = function (legacyOptions) {
const getItemByItemGroup = function (itemGroupId) {
if (typeof itemGroupId === 'object') throw new Error('Expecting String or number (Item group id)');
if (!itemGroupId) throw new Error('Error Item Group ID is required');
if (!this.options.access_token) throw new Error('Missing Access token, Generate access token');
const auth = 'Bearer ' + this.options.access_token;
if (!this.options.appAccessToken) throw new Error('Missing Access token, Generate access token');
const auth = 'Bearer ' + this.options.appAccessToken;
this.options.contentType = 'application/json';
return new Promise((resolve, reject) => {
makeRequest(this.options, `/buy/browse/v1/item/get_items_by_item_group?item_group_id=${itemGroupId}`, 'GET', auth).then((result) => {
Expand All @@ -50,8 +50,8 @@ const getItemByItemGroup = function (itemGroupId) {
const searchItems = function (searchConfig) {
if (!searchConfig) throw new Error('Error --> Missing or invalid input parameter to search');
if (!searchConfig.keyword && !searchConfig.categoryId && !searchConfig.gtin) throw new Error('Error --> Keyword or category id is required in query param');
if (!this.options.access_token) throw new Error('Error -->Missing Access token, Generate access token');
const auth = 'Bearer ' + this.options.access_token;
if (!this.options.appAccessToken) throw new Error('Error -->Missing Access token, Generate access token');
const auth = 'Bearer ' + this.options.appAccessToken;
let queryParam = searchConfig.keyword ? 'q=' + encodeURIComponent(searchConfig.keyword) : '';
queryParam = queryParam + (searchConfig.gtin ? '&gtin=' + searchConfig.gtin : '');
queryParam = queryParam + (searchConfig.categoryId ? '&category_ids=' + searchConfig.categoryId : '');
Expand All @@ -73,9 +73,9 @@ const searchItems = function (searchConfig) {

const searchByImage = function (searchConfig) {
if (!searchConfig) throw new Error('INVALID_REQUEST_PARMS --> Missing or invalid input parameter to search by image');
if (!this.options.access_token) throw new Error('INVALID_AUTH_TOKEN --> Missing Access token, Generate access token');
if (!this.options.appAccessToken) throw new Error('INVALID_AUTH_TOKEN --> Missing Access token, Generate access token');
if (!searchConfig.imgPath && !searchConfig.base64Image) throw new Error('REQUIRED_PARAMS --> imgPath or base64Image is required');
const auth = 'Bearer ' + this.options.access_token;
const auth = 'Bearer ' + this.options.appAccessToken;
const encodeImage = searchConfig.imgPath ? base64Encode(fs.readFileSync(searchConfig.imgPath)) : searchConfig.base64Image;
this.options.data = JSON.stringify({ image: encodeImage });
this.options.contentType = 'application/json';
Expand Down
22 changes: 1 addition & 21 deletions src/common-utils/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';
const { makeRequest } = require('../request');

const base64Encode = (encodeData) => {
const base64Encode = encodeData => {
const buff = Buffer.from(encodeData);;
return buff.toString('base64');
};
Expand Down Expand Up @@ -46,23 +46,6 @@ const constructAdditionalParams = (options) => {
};

module.exports = {
setAccessToken: function (token) {
this.options.access_token = token;
},
getAccessToken: function () {
if (!this.options.clientID) throw new Error('Missing Client ID');
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
if (!this.options.body) throw new Error('Missing Body, required Grant type');
const encodedStr = base64Encode(this.options.clientID + ':' + this.options.clientSecret);
const self = this;
const auth = 'Basic ' + encodedStr;
this.options.contentType = 'application/x-www-form-urlencoded';
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then((result) => {
const resultJSON = JSON.parse(result);
self.setAccessToken(resultJSON.access_token);
return resultJSON;
});
},
setSiteId: function (siteId) {
this.options.siteId = siteId;
},
Expand All @@ -73,10 +56,7 @@ module.exports = {
if (!isString(data)) data = data.toString();
return data.toUpperCase();
},

// Returns if a value is a string
isString,

// Returns if object is empty or not
isEmptyObj(obj) {
for (let key in obj) {
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

module.exports = {
PROD_OAUTHENVIRONMENT_WEBENDPOINT: 'https://auth.ebay.com/oauth2/authorize',
SANDBOX_OAUTHENVIRONMENT_WEBENDPOINT: 'https://auth.sandbox.ebay.com/oauth2/authorize',
PROD_BASE_URL: 'api.ebay.com',
SANDBOX_BASE_URL: 'api.sandbox.ebay.com',
BASE_SVC_URL: 'svcs.ebay.com',
Expand Down
151 changes: 151 additions & 0 deletions src/credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use strict';
const qs = require('querystring');
const { base64Encode } = require('./common-utils');
const { makeRequest } = require('./request');
const DEFAULT_API_SCOPE = 'https://api.ebay.com/oauth/api_scope';

/**
* Generates an application access token for client credentials grant flow
*
* @return appAccessToken object
*/
const getAccessToken = function () {
if (!this.options.clientID) throw new Error('Missing Client ID');
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
if (!this.options.body) throw new Error('Missing Body, required Grant type');
let scopesParam = this.options.body.scopes
? Array.isArray(this.options.body.scopes)
? this.options.body.scopes.join('%20')
: this.options.body.scopes
: DEFAULT_API_SCOPE;
this.options.data = qs.stringify({
grant_type: 'client_credentials',
scope: scopesParam
});
this.options.contentType = 'application/x-www-form-urlencoded';
const self = this;
const encodedStr = base64Encode(this.options.clientID + ':' + this.options.clientSecret);
const auth = 'Basic ' + encodedStr;
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then((result) => {
const resultJSON = JSON.parse(result);
if (!resultJSON.error) self.setAppAccessToken(resultJSON);
return resultJSON;
});
};

/**
* Generates user consent authorization url
*
* @param state custom state value
* @return userConsentUrl
*/
const getUserAuthorizationUrl = function (state = null) {
if (!this.options.clientID) throw new Error('Missing Client ID');
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
if (!this.options.body) throw new Error('Missing Body, required Grant type');
if (!this.options.redirectUri) throw new Error('redirect_uri is required for redirection after sign in\nkindly check here https://developer.ebay.com/api-docs/static/oauth-redirect-uri.html');
let scopesParam = this.options.body.scopes
? Array.isArray(this.options.body.scopes)
? this.options.body.scopes.join('%20')
: this.options.body.scopes
: DEFAULT_API_SCOPE;
let queryParam = `client_id=${this.options.clientID}`;
queryParam += `&redirect_uri=${this.options.redirectUri}`;
queryParam += `&response_type=code`;
queryParam += `&scope=${scopesParam}`;
queryParam += state ? `&state=${state}` : '';
return `${this.options.oauthEndpoint}?${queryParam}`;
};

/**
* Generates a User access token given auth code
*
* @param code code generated from browser using the method getUserAuthorizationUrl (should be urldecoded)
* @return userAccessToken object (with refresh_token)
*/
const getUserTokenByCode = function (code) {
if (!code) throw new Error('Authorization code is required, to generate authorization code use getUserAuthorizationUrl method');
if (!this.options.clientID) throw new Error('Missing Client ID');
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
if (!this.options.redirectUri) throw new Error('redirect_uri is required for redirection after sign in\nkindly check here https://developer.ebay.com/api-docs/static/oauth-redirect-uri.html');
this.options.data = qs.stringify({
code: code,
grant_type: 'authorization_code',
redirect_uri: this.options.redirectUri
});
this.options.contentType = 'application/x-www-form-urlencoded';
const self = this;
const encodedStr = base64Encode(`${this.options.clientID}:${this.options.clientSecret}`);
const auth = `Basic ${encodedStr}`;
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then(result => {
const resultJSON = JSON.parse(result);
if (!resultJSON.error) self.setUserAccessToken(resultJSON);
return resultJSON;
});
};

/**
* Use a refresh token to update a User access token (Updating the expired access token)
*
* @param refreshToken refresh token, defaults to pre-assigned refresh token
* @param scopes array of scopes for the access token
* @return userAccessToken object (without refresh_token)
*/
const getUserTokenByRefresh = function (refreshToken = null) {
if (!this.options.clientID) throw new Error('Missing Client ID');
if (!this.options.clientSecret) throw new Error('Missing Client Secret or Cert Id');
if (!this.options.body) throw new Error('Missing Body, required Grant type');
if (!refreshToken && !this.options.refreshToken) {
throw new Error('Refresh token is required, to generate refresh token use getUserTokenByCode method'); // eslint-disable-line max-len
}
refreshToken = refreshToken ? refreshToken : this.options.refreshToken;
let scopesParam = this.options.body.scopes
? Array.isArray(this.options.body.scopes)
? this.options.body.scopes.join('%20')
: this.options.body.scopes
: DEFAULT_API_SCOPE;
this.options.data = qs.stringify({
refresh_token: refreshToken,
grant_type: 'refresh_token',
scope: scopesParam
});
this.options.contentType = 'application/x-www-form-urlencoded';
const self = this;
const encodedStr = base64Encode(`${this.options.clientID}:${this.options.clientSecret}`);
const auth = `Basic ${encodedStr}`;
return makeRequest(this.options, '/identity/v1/oauth2/token', 'POST', auth).then(result => {
const resultJSON = JSON.parse(result);
if (!resultJSON.error) self.setUserAccessToken(resultJSON);
return resultJSON;
});
};

/**
* Assign user access token and refresh token returned from authorization grant workflow (i.e getUserTokenByRefresh)
*
* @param userAccessToken userAccessToken obj returned from getUserTokenByCode or getAccessTokenByRefresh
*/
const setUserAccessToken = function (userAccessToken) {
if (!userAccessToken.token_type === 'User Access Token') throw new Error('userAccessToken is either missing or invalid');
if (userAccessToken.refresh_token) this.options.refreshToken = userAccessToken.refresh_token;
this.options.userAccessToken = userAccessToken.access_token;
};

/**
* Assign application access token returned from client credentials workflow (i.e getAccessToken)
*
* @param appAccessToken appAccessToken obj returned from getApplicationToken
*/
const setAppAccessToken = function (appAccessToken) {
if (!appAccessToken.token_type === 'Application Access Token') throw new Error('appAccessToken is either missing or invalid');
this.options.appAccessToken = appAccessToken.access_token;
};

module.exports = {
getAccessToken,
getUserAuthorizationUrl,
getUserTokenByCode,
getUserTokenByRefresh,
setUserAccessToken,
setAppAccessToken
};
27 changes: 25 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ const taxonomyApi = require('./taxonomy-api');
const ebayFindingApi = require('./finding');
const commonUtils = require('./common-utils');
const { getSimilarItems, getMostWatchedItems } = require('./merchandising');
const { PROD_BASE_URL, SANDBOX_BASE_URL, BASE_SANDBX_SVC_URL, BASE_SVC_URL } = require('./constants');
const {
getAccessToken,
getUserAuthorizationUrl,
getUserTokenByCode,
getUserTokenByRefresh,
setAppAccessToken,
setUserAccessToken
} = require('./credentials');
const {
PROD_OAUTHENVIRONMENT_WEBENDPOINT,
SANDBOX_OAUTHENVIRONMENT_WEBENDPOINT,
PROD_BASE_URL,
SANDBOX_BASE_URL,
BASE_SANDBX_SVC_URL,
BASE_SVC_URL
} = require('./constants');
const PROD_ENV = 'PROD';
const SANDBOX_ENV = 'SANDBOX';

Expand All @@ -15,23 +30,25 @@ const SANDBOX_ENV = 'SANDBOX';
*
* @param {Object} options configuration options
* @param {String} options.clientID Client Id/App id
* @param {String} options.clientSecret eBay Secret/Cert ID - required for user access tokens
* @param {String} options.env Environment, defaults to PROD
* @param {String} options.headers HTTP request headers
* @constructor
* @public
*/

function Ebay(options) {
if (!options) throw new Error('Options is missing, please provide the input');
if (!options.clientID) throw Error('Client ID is Missing\ncheck documentation to get Client ID http://developer.ebay.com/DevZone/account/');
if (!(this instanceof Ebay)) return new Ebay(options);
if (!options.env) options.env = PROD_ENV;
options.baseUrl = PROD_BASE_URL;
options.baseSvcUrl = BASE_SVC_URL;
options.oauthEndpoint = PROD_OAUTHENVIRONMENT_WEBENDPOINT;
// handle sandbox env.
if (options.env === SANDBOX_ENV) {
options.baseUrl = SANDBOX_BASE_URL;
options.baseSvcUrl = BASE_SANDBX_SVC_URL;
options.oauthEndpoint = SANDBOX_OAUTHENVIRONMENT_WEBENDPOINT;
}
this.options = options;
commonUtils.setHeaders(this, options.headers);
Expand All @@ -40,6 +57,12 @@ function Ebay(options) {
}

Ebay.prototype = {
getAccessToken,
getUserAuthorizationUrl,
getUserTokenByCode,
getUserTokenByRefresh,
setUserAccessToken,
setAppAccessToken,
getMostWatchedItems,
getSimilarItems,
...commonUtils,
Expand Down
Loading

0 comments on commit d389d49

Please sign in to comment.