shopee-v2-api
Version:
691 lines (615 loc) • 19.3 kB
JavaScript
/* -------------------------------------------------------------------------- */
/* Shopee API List */
/* -------------------------------------------------------------------------- */
const Crypto = require('crypto');
const Fetch = require('node-fetch');
const Moment = require('moment');
const _ = require('lodash');
/* -------------------------------------------------------------------------- */
/* Constants */
/* -------------------------------------------------------------------------- */
const {
constants
} = require('./constants');
const {
request
} = require('http');
/* -------------------------------------------------------------------------- */
/* Common */
/* -------------------------------------------------------------------------- */
/**
* 암호화
* @param {*} str
* @param {*} key
* @returns
*/
function genHMACSHA256(str, key) {
return Crypto.createHmac('sha256', key).update(str).digest('hex');
};
/**
* sign key 생성
* @param {*} path
* @param {*} access_token
* @param {*} shop_id
* @returns sign key, timestamp
*/
function buildSignKey(path, access_token = null, shop_id = null) {
const currentTimeStamp = Moment().unix();
// sign key 생성
let baseString = constants.partnerId + path + currentTimeStamp;
// console.log('>>>>>>> baseString: ' + baseString);
if (access_token) {
baseString += access_token;
}
console.log('>>>>>>> token: ' + access_token);
if (shop_id) {
baseString += shop_id;
}
const signKey = genHMACSHA256(baseString, constants.key);
console.log('>>>>>>> sign: ' + signKey);
return { signKey, currentTimeStamp };
}
class HTTPResponseError extends Error {
constructor(response, ...args) {
super(`${response.status} ${response.statusText}`, ...args);
this.response = response;
}
}
function logError(response, payload, responseData) {
// 에러 로그
console.error({
url: response.url
});
if (payload) {
console.error({
payload: payload
});
}
console.error(responseData);
}
/**
* 요청 결과 오류 확인 및 json 파싱
* @param {*} response
* @returns
*/
async function parseResponse(response, payload = null) {
if (response.ok) {
let responseData = await response.json();
if (process.env.NODE_DEBUG === 'true') {
// debug log
console.log(JSON.stringify(responseData));
}
if (responseData.error) {
logError(response, payload, responseData);
// 에러 실행
throw new HTTPResponseError({
status: response.status,
statusText: JSON.stringify(responseData)
}, responseData);
} else {
return _.assign(responseData, {
fetch: { status: response.status, url: response.url }
});
}
} else {
if (process.env.NODE_DEBUG === 'true') {
// debug log
console.log(`${response.status} ${response.statusText}`);
}
// access token 이 유효하지 않음
if (response.status === 403) {
// const params = new URLSearchParams(response.url);
// 만료된 토큰 삭제
// if (params.get('shop_id') && Number(params.get('shop_id')) > 0) {
// await User_shop.update({
// shop_id: Number(params.get('shop_id'))
// }, {
// accessToken: null,
// tokenExpireIn: 0
// });
// } else if (params.get('merchant_id') && Number(params.get('merchant_id')) > 0) {
// await User_merchant.update({
// merchant_id: Number(params.get('merchant_id'))
// }, {
// accessToken: null,
// tokenExpireIn: 0
// });
// }
return {
error: response.statusText,
message: 'invalid token',
};
} else {
if (process.env.NODE_DEBUG === 'true') {
// debug log
console.log(response);
}
// 에러 로그
console.error({ url: response.url });
// 에러 실행
throw new HTTPResponseError({
status: response.status,
statusText: response.statusText,
});
}
}
}
/**
* 연동 이후 호출되는 웹 주소
* @returns
*/
exports.GetRedirectWebUrl = (params) => {
return constants.redirectWeb + params;
};
/**
* API URL 생성
* @param {*} uri
* @param {*} options
* @returns
*/
function buildUrl(uri, options) {
// console.log(options);
let url = `${constants.endPoint}${uri}`;
const queryString = Object.keys(options)
.filter(key => options[key] !== null && options[key] !== undefined)
.map((key, index) => `${index === 0 ? '?' : '&'}${key}=${options[key]}`)
.join('');
// console.log('>>>>>>> url: ' + url);
return url + queryString;
}
/**
* common request header
* @returns
*/
function header() {
return { 'Content-Type': 'application/json' };
}
/**
* get 요청
* @param {*} url
*/
async function get(url) {
const timestamp = new Date().getTime();
console.time(`[${timestamp}] ${url}`);
// request
const response = await Fetch(url, {
method: 'get',
headers: header(),
});
console.timeEnd(`[${timestamp}] ${url}`);
// response data
return await parseResponse(response);
}
/**
* post 요청
* @param {*} url
* @param {*} body
* @returns
*/
async function post(url, body) {
const timestamp = new Date().getTime();
console.time(`[${timestamp}] ${url}`);
if (process.env.NODE_DEBUG === 'true') {
// debug log
// console.log(body);
}
// request
const response = await Fetch(url, {
method: 'post',
headers: header(),
body: body
});
console.timeEnd(`[${timestamp}] ${url}`);
// response data
return await parseResponse(response, body);
}
/**
* 파일 업로드
* @param {*} url
* @param {*} body
* @returns
*/
async function postFile(url, body) {
const timestamp = new Date().getTime();
console.time(`[${timestamp}] ${url}`);
// request
const response = await Fetch(url, {
method: 'post',
body: body
});
console.timeEnd(`[${timestamp}] ${url}`);
// response data
return await parseResponse(response);
}
/* -------------------------------------------------------------------------- */
/* API */
/* -------------------------------------------------------------------------- */
/*
shopee V2 api 방식
- 인증 이후 받은 code 로 access token 요청
- access token(4시간 유효) 으로 API 이용
- access token 만료되면 refresh token(30일 유효) 으로 재요청
- refresh token 만료되면 다시 인증!
- Common Parameters 는 uri 에 query string 으로 전달
- 각 API 별 Request parameters 는 기존과 동일
*/
/* ----------------------------------- 인증 ----------------------------------- */
/**
* 인증
* @param {*} userId
* @param {*} version
* @returns url
*/
exports.Authorize = (userId, version) => {
// auth uri
const uri = '/api/v2/shop/auth_partner';
// callback url
const callbackUrl = `${constants.redirectUrl}?data=${userId}|${version}`;
// build key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri);
// authorize 주소
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey,
redirect: encodeURI(callbackUrl)
});
return url;
};
/**
* access token 요청
* @param {*} shopId
* @param {*} code
* @returns response data
*/
async function GetToken(shopId, code) {
try {
// uri
const uri = '/api/v2/auth/token/get';
// sign key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri);
// url
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey
});
// request && response data
return await post(url, JSON.stringify({
code: code,
partner_id: constants.partnerId,
shop_id: shopId
}));
} catch (err) {
this.handleRequestError(err);
}
}
/**
* access token 유효 확인 및 갱신, 저장
* @param {*} userShop
* @returns access token
*/
async function RefreshToken(userShop) {
try {
// access token 유무 및 만료 확인
if (userShop.accessToken &&
userShop.tokenExpireIn > Moment().unix()) {
return userShop; // 유효한 토큰
} else {
if (!userShop.refreshToken) {
// refresh 토큰이 없을 경우
return userShop;
}
// access token 재발급
// uri
const uri = '/api/v2/auth/access_token/get';
// sign key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri);
// url
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey
});
// params
let params = {
refresh_token: userShop.refreshToken,
partner_id: constants.partnerId,
shop_id: userShop.shop_id
};
if (process.env.NODE_DEBUG === 'true') {
// debug log
console.log(params);
}
// request && response data
const responseData = await post(url, JSON.stringify(params));
if (responseData.access_token && responseData.refresh_token) {
// 현재 날짜
let now = Moment();
// 새 토큰 저장
const us = await User_shop.update({
shop_id: userShop.shop_id
}, {
accessToken: responseData.access_token,
refreshToken: responseData.refresh_token,
tokenExpireIn: now.unix() + Number(responseData.expire_in),
refreshTokenExpireIn: now.add(30, 'day').unix(),
});
return us[0];
} else {
return userShop;
}
}
} catch (err) {
this.handleRequestError(err);
}
}
/**
* merchant - access token 유효 확인 및 갱신, 저장
* @param {*} userShop
* @returns
*/
async function GPRefreshToken(userMerchant) {
try {
// console.log('>>>>>>>>>>>>>>>>>> GPRefreshToken');
// // access token 유무 및 만료 확인
// if (userMerchant.accessToken &&
// userMerchant.tokenExpireIn > Moment().unix()) {
// return userMerchant; // 유효한 토큰
// } else {
if (!userMerchant.refreshToken) {
// refresh token 이 없는 경우
return userMerchant;
}
// access token 재발급
// uri
const uri = '/api/v2/auth/access_token/get';
// sign key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri);
// url
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey
});
// params
let params = {
refresh_token: userMerchant.refreshToken,
partner_id: constants.partnerId,
merchant_id: userMerchant.merchant_id
};
if (process.env.NODE_DEBUG === 'true') {
// debug log
// console.log(params);
}
// request && response data
const responseData = await post(url, JSON.stringify(params));
if (responseData.access_token && responseData.refresh_token) {
// 현재 날짜
let now = Moment();
// 새 토큰 저장
const um = await User_merchant.update({
merchant_id: userMerchant.merchant_id
}, {
accessToken: responseData.access_token,
refreshToken: responseData.refresh_token,
tokenExpireIn: now.unix() + Number(responseData.expire_in),
refreshTokenExpireIn: now.add(30, 'day').unix(),
});
return um[0];
} else {
return userMerchant;
}
// }
} catch (err) {
if (err.response) {
throw _.assign(new Error(err.message), {
status: err.response.status,
statusText: err.response.statusText,
response: typeof err.response.json === 'function' ? await err.response.json() : null
});
} else {
throw err;
}
}
}
/* ------------------------------ Main account ------------------------------ */
/**
* main account access token 요청
* @param {*} mainAccountId
* @param {*} code
* @returns response data
*/
exports.MAGetToken = async (mainAccountId, code) => {
try {
// uri
const uri = '/api/v2/auth/token/get';
// sign key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri);
// url
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey
});
// request && response data
const responseData = await post(url, JSON.stringify({
code: code,
partner_id: constants.partnerId,
main_account_id: mainAccountId
}));
return responseData;
} catch (err) {
this.handleRequestError(err);
}
};
/* -------------------------------- Merchant -------------------------------- */
/**
* 최초 머천트 정보 조회시 사용
* @param {*} userMerchant
* @returns
*/
exports.GetMerchantInfo = async (userMerchant) => {
try {
/* --------------------------- access token 신규 발급 --------------------------- */
// uri
const tokenUri = '/api/v2/auth/access_token/get';
// sign key
const tokenSign = buildSignKey(tokenUri);
// url
const tokenUrl = buildUrl(tokenUri, {
partner_id: constants.partnerId,
timestamp: tokenSign.currentTimeStamp,
sign: tokenSign.signKey
});
// params
let params = {
refresh_token: userMerchant.refreshToken,
partner_id: constants.partnerId,
merchant_id: userMerchant.merchant_id
};
// request && response data
const responseTokenData = await post(tokenUrl, JSON.stringify(params));
/* --------------------------------- 머천트 정보 --------------------------------- */
// uri
const uri = '/api/v2/merchant/get_merchant_info';
// sign key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri, responseTokenData.access_token, userMerchant.merchant_id);
// url
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey,
access_token: responseTokenData.access_token,
merchant_id: userMerchant.merchant_id
});
// request && response data
const responseData = await get(url);
return {
token: responseTokenData,
info: responseData
};
} catch (err) {
throw err;
}
};
/**
* 머천트내 shop 목록
* @param {*} userMerchant
* @returns
*/
exports.GetShopListByMerchant = async (userMerchant) => {
try {
// access token 유효 확인
let {
merchant_id,
accessToken
} = await GPRefreshToken(userMerchant);
// uri
const uri = '/api/v2/merchant/get_shop_list_by_merchant';
// sign key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri, accessToken, merchant_id);
// list
let shopList = [];
let hasMore = true;
let pagePerNo = 300;
while (hasMore) {
// url
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey,
access_token: accessToken,
merchant_id: merchant_id,
page_no: 1,
page_size: pagePerNo
});
// request && response data
const responseData = await get(url);
if (responseData.shop_list && responseData.shop_list.length > 0) {
shopList = shopList.concat(responseData.shop_list);
}
hasMore = responseData.more;
}
return shopList;
} catch (err) {
throw err;
}
}
/* ---------------------------------- Shop ---------------------------------- */
/**
* 상점 정보
* @param {*} userShop
* @returns
*/
exports.GetShopInfo = async (userShop) => {
try {
// access token 유효 확인
let {
shop_id,
accessToken
} = await RefreshToken(userShop);
// uri
const uri = '/api/v2/shop/get_shop_info';
// sign key
const {
signKey,
currentTimeStamp
} = buildSignKey(uri, accessToken, shop_id);
// url
const url = buildUrl(uri, {
partner_id: constants.partnerId,
timestamp: currentTimeStamp,
sign: signKey,
access_token: accessToken,
shop_id: shop_id
});
// request && response data
const responseData = await get(url);
return responseData;
} catch (err) {
throw err;
}
};
exports.handleRequestError = async (err) => {
if (err.response) {
throw _.assign(new Error(err.message), {
status: err.response.status,
statusText: err.response.statusText,
response: typeof err.response.json === 'function' ? await err.response.json() : null
});
} else {
throw err;
}
}
/* --------------------------------- exports -------------------------------- */
exports.get = get;
exports.post = post;
exports.postFile = postFile;
exports.buildUrl = buildUrl;
exports.buildSignKey = buildSignKey;
exports.parseResponse = parseResponse;
exports.GetToken = GetToken;
exports.RefreshToken = RefreshToken;
exports.GPRefreshToken = GPRefreshToken;