UNPKG

shopee-v2-api

Version:

691 lines (615 loc) 19.3 kB
/* -------------------------------------------------------------------------- */ /* 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;