UNPKG

@blockv/sdk

Version:

Allows web apps to display and interact with vatoms.

182 lines (162 loc) 6.72 kB
// // BlockV AG. Copyright (c) 2018, all rights reserved. // // Licensed under the BlockV SDK License (the "License"); you may not use this file or // the BlockV SDK except in compliance with the License accompanying it. Unless // required by applicable law or agreed to in writing, the BlockV SDK distributed under // the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF // ANY KIND, either express or implied. See the License for the specific language // governing permissions and limitations under the License. // import fetch from '@brillout/fetch' import jwtDecode from 'jwt-decode' /* global FormData */ /** List of known error messages to replace the server-supplied ones */ const ErrorCodes = { 2: 'Blank App ID', 17: 'Invalid App ID', 401: 'Token has Expired', 516: 'Invalid Payload', 517: 'Invalid Payload', 521: 'Token Unavailable', 527: 'Invalid Date Format', 1004: 'Invalid Request Payload', 1701: 'vAtom Unrecognized', 1708: 'vAtom Unvailable', 2030: 'No user found, Please register an account first.', 2031: 'Authentication Failed', 2032: 'Login Failed, Please try again', 2034: 'Invalid Token', 2037: 'Upload Avatar Failed', 2049: 'Refresh Token Expired / Not Whitelisted', 2051: 'Too many login attempts, Please try again later.', 2552: 'Unable To Retrieve Token', 2553: 'Token ID Invalid', 2562: 'Cannot Delete Primary Token', 2563: 'Token Already Confirmed', 2564: 'Invalid Verification Code', 2566: 'Token Already Confirmed', 2567: 'Invalid Verification Code', 2569: 'Invalid Token Type', 2571: 'Invalid Email', 2572: 'Invalid Phone Number' } export default class Client { /** @private */ constructor (bv) { this.Blockv = bv this.store = bv.store } /** * Sends a request to the backend. * @param {string} method The HTTP method, ie. "GET", "POST" * @param {string} endpoint The backend API endpoint, ie. "/v1/user" * @param {string|object|FormData} payload The request body. Can be null. * @param {boolean} auth `true` if this request should contain the current user's access token. * @param {object} headers Optional extra HTTP headers to add to the request. * @returns {Promise<object>} The server's response payload. */ async request (method, endpoint, payload, auth, headers) { // Ensure our access token is up to date, if this is an authenticated request if (auth) await this.checkToken() // Send request return this.authRequest(method, endpoint, payload, headers) } /** @private */ async authRequest (method, endpoint, payload, extraHeaders) { // Setup headers const headers = Object.assign({ 'App-Id': this.store.appID, Authorization: `Bearer ${this.store.token}` }, extraHeaders) // Check payload type let body = null if (!payload) { // If no body, make it undefined so that fetch() doesn't complain about having a payload on GET requests body = undefined } else if (typeof FormData !== 'undefined' && payload instanceof FormData) { // Don't add Content-Type header, fetch() adds it's own, which is required because it specifies the form data boundary body = payload } else if (typeof body === 'object') { // Convert to JSON body = JSON.stringify(payload) headers['Content-Type'] = 'application/json' } else { // Unknown payload type, assume application/json content type, unless specified in extra headers body = payload if (!extraHeaders['Content-Type']) headers['Content-Type'] = 'application/json' } // Send request const response = await fetch(this.store.server + endpoint, { method, body, headers }) // Decode JSON const json = await response.json() // Check for server error if (!json.payload && json.error === 2051) { // Check for the special login locked error // We need to pull the timestamp that is in the reponse.message to show when they // can login agin // HACK: Pull time from original server error string const dateString = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/g.exec(response.message) response.lockedUntil = new Date(dateString) // Replace duration in the error message let duration = response.lockedUntil.getTime() - Date.now() if (duration < 2000) duration = 2000 const seconds = Math.floor(duration / 1000) const minutes = Math.floor(duration / 1000 / 60) if (seconds <= 90) { response.message = response.message.replace('%DURATION%', seconds === 1 ? '1 second' : `${seconds} seconds`) } else { response.message = response.message.replace('%DURATION%', minutes === 1 ? '1 minute' : `${minutes} minutes`) } // Throw error const error = new Error(`Too many login attempts, Try again at : ${response.lockedUntil}`) error.code = json.error || response.status || 0 error.httpStatus = response.status throw error } else if (!json.payload) { // Throw the error returned by the server const error = new Error(ErrorCodes[response.error] || json.message || 'An unknown server error has occurred') error.code = json.error || response.status || 0 error.httpStatus = response.status throw error } // No error, continue return json.payload } /** * Uses the refresh token to fetch and store a new access token from the backend. * @private */ async refreshToken () { // Fetch new access token const data = await this.request('POST', '/v1/access_token', '', false, { Authorization: `Bearer ${this.store.refreshToken}` }) // Store it this.store.token = data.access_token.token } /** * Checks if the current access token is still valid and has not expired yet. If it is, it will fetch a new one. * @private * @returns {Promise} Resolves when the access token is valid. */ async checkToken () { // define our vars let decodedToken let nowDate let expirationTime // Catch errors with decoding the current access token try { decodedToken = jwtDecode(this.store.token) expirationTime = (decodedToken.exp * 1000) nowDate = Date.now() // quick calc to determine if the token has expired if ((nowDate + 5000) > expirationTime) throw new Error('Token expired.') } catch (e) { // There was an error with the access token. Fetch a new one. return this.refreshToken() } // Done return true } }