Last active
December 7, 2017 16:06
-
-
Save abiodun0/670dc97841d7c1e97e66ca6c58e6ac75 to your computer and use it in GitHub Desktop.
some interesting api abstraction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Path from 'path-parser' | |
import { isTokenExpired } from './auth0Utils' | |
import { HttpForbiddenError, UnauthorizedError } from './errors' | |
const GET = 'GET' | |
const PUT = 'PUT' | |
const POST = 'POST' | |
const PATCH = 'PATCH' | |
const DELETE = 'DELETE' | |
const FANCY_METHODS = new Set([ PATCH, PUT, DELETE ]) | |
const endpoint = (method = GET, authenticated = true) => route => { | |
return { | |
method, | |
authenticated, | |
route | |
} | |
} | |
// @formatter:off | |
/* eslint key-spacing: 0, func-call-spacing: 0, no-multi-spaces: 0 */ | |
const API_DEFNS = { | |
/* Authenticated Routes */ | |
account: { | |
get: endpoint(GET) ('/account'), | |
goals: endpoint(GET) ('/account/goals'), | |
fields: endpoint(GET) ('/fields'), | |
update: endpoint(PATCH) ('/account') | |
}, | |
audience: { | |
list: endpoint(GET) ('/audiences/aperture'), | |
get: endpoint(GET) ('/audiences/:id/aperture_fast'), | |
getDetails: endpoint(GET) ('/audiences/:id/aperture'), | |
create: endpoint(POST) ('/audiences'), | |
update: endpoint(PATCH) ('/audiences/:id/aperture') | |
}, | |
campaigns: { | |
deliver: endpoint(POST) ('/campaigns/:id/deliveries'), | |
list: endpoint(GET) ('/campaigns/aperture'), | |
get: endpoint(GET) ('/campaigns/:id'), | |
update: endpoint(PATCH) ('/campaigns/:id') | |
}, | |
contexts: { | |
get: endpoint(GET) ('/mars/contexts/:id') | |
}, | |
deliveries: { | |
get: endpoint(GET) ('/deliveries/:id'), | |
url: endpoint(GET) ('/deliveries/:id/url'), | |
preapprove: endpoint(POST) ('/deliveries/:id/preapprove') | |
}, | |
ensembles: { | |
list: endpoint(GET) ('/ensembles') | |
}, | |
files: { | |
list: endpoint(GET) ('/customer_files'), | |
get: endpoint(GET) ('/customer_files/:id'), | |
url: endpoint(GET) ('/customer_files/:id/url'), | |
create: endpoint(POST) ('/customer_files'), | |
report: endpoint(GET) ('/customer_files/:id/customer_file_report') | |
}, | |
places: { | |
search: endpoint(GET) ('/places?q') | |
}, | |
pointsOfInterest: { | |
list: endpoint(GET) ('/points_of_interest') | |
}, | |
subscription: { | |
get: endpoint(GET) ('/subscription') | |
}, | |
user: { | |
get: endpoint(GET) ('/user'), | |
update: endpoint(PATCH) ('/user') | |
}, | |
users: { | |
list: endpoint(GET) ('/users'), | |
create: endpoint(POST) ('/users'), | |
kick: endpoint(POST) ('/users/:id/kick') | |
}, | |
vendors: { | |
list: endpoint(GET) ('/vendors'), | |
create: endpoint(POST) ('/vendors/:id/setting'), | |
update: endpoint(PATCH) ('/vendors/:id/setting'), | |
delete: endpoint(DELETE)('/vendors/:id/setting') | |
}, | |
/* Unauthenticated Routes */ | |
password: { | |
requestReset: endpoint(POST, false)('/password_resets'), | |
reset: endpoint(PUT, false)('/password_resets/:token') | |
}, | |
session: { | |
create: endpoint(POST, false)('/sessions'), | |
social: endpoint(POST, true) ('/social_sessions') // note, requires auth0 token to complete | |
}, | |
signup: { | |
complete: endpoint(POST, false)('/user/registration_completion?token') | |
} | |
} | |
// @formatter:on | |
const PREFIX = '/api' | |
const STANDARD_HEADERS = { | |
'Content-Type': 'application/json', | |
Accept: 'application/json' | |
} | |
export class SimpleAuthService { | |
async getToken () { | |
const token = localStorage.token | |
if (this.loggingOut) { | |
return new Promise(() => {}) // never resolve token | |
} else if (!token || isTokenExpired(token)) { | |
// todo: refresh token if possible | |
this.kick() | |
throw new UnauthorizedError() | |
} else { | |
return token | |
} | |
} | |
kick () { | |
if (!this.loggingOut) { | |
delete localStorage.token | |
delete localStorage.profile | |
window.location = '/login/' // kick user | |
this.loggingOut = true | |
} | |
} | |
} | |
export class ApiService { | |
// Split incoming args into URL args and body args: | |
static splitArgs (method, args, path) { | |
const url = path.build(args) | |
if (method === GET || !args) return { url, body: null } | |
else { | |
const pathArgs = path.test(url) | |
const body = Object.entries(args).reduce((args, [argKey, argVal]) => | |
(argKey in pathArgs) | |
? args | |
: { ...args, [argKey]: argVal }, | |
{}) | |
return { url, body } | |
} | |
} | |
static async getBody (response) { | |
const responseBody = await response.text() | |
if (responseBody.trim()) { // sometimes we get empty responses back from Flora | |
return JSON.parse(responseBody) | |
} else { | |
return null | |
} | |
} | |
constructor (authService) { | |
this.authService = authService | |
Object.entries(API_DEFNS).forEach(([subject, value]) => { | |
if (!(subject in this)) this[subject] = {} | |
Object.entries(value).forEach(([endpoint, {method, authenticated, route}]) => { | |
const path = new Path(`${PREFIX}${route}`) | |
this[subject][endpoint] = (args) => { | |
const { url, body } = ApiService.splitArgs(method, args, path) | |
return this.request({ | |
method, | |
url, | |
body, | |
authenticated | |
}) | |
} | |
}) | |
}) | |
} | |
async request ({ method, url, body, authenticated }) { | |
const finalHeaders = {...STANDARD_HEADERS} | |
if (FANCY_METHODS.has(method)) { | |
finalHeaders['X-HTTP-Method-Override'] = method | |
} | |
if (authenticated) { | |
finalHeaders['Authorization'] = `Bearer ${await this.authService.getToken()}` | |
} | |
const response = await fetch(url, { | |
method, | |
headers: finalHeaders, | |
body: body ? JSON.stringify(body) : undefined | |
}) | |
if (response.ok) { | |
return ApiService.getBody(response) | |
} else if (response.status === 503) { | |
window.location.reload() | |
throw new Error('Flora gone into maintenence mode') | |
} else if (authenticated && (response.status === 401 || (url === '/api/account' && response.status === 404))) { | |
this.authService.kick() | |
return new Promise(() => {}) | |
} else if (response.status === 403) { | |
throw new HttpForbiddenError(response) | |
} else { | |
const responseBody = await ApiService.getBody(response) | |
const e = new Error(response.statusText) | |
e.details = responseBody | |
throw e | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment