Files
deighton-ar/mobile/src/API.js
John Lyon-Smith 9730c83c9c Bug fixing
2018-04-23 14:01:52 -07:00

482 lines
12 KiB
JavaScript

import EventEmitter from "eventemitter3"
import io from "socket.io-client"
import { AsyncStorage } from "react-native"
import autobind from "autobind-decorator"
import { config } from "./config"
const authTokenKeyName = "AuthToken"
const backendKeyName = "Backend"
class NetworkError extends Error {
constructor(message) {
super(message)
this.name = this.constructor.name
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = new Error(message).stack
}
}
}
class APIError extends Error {
constructor(status, message) {
super(message || "")
this.status = status || 500
this.name = this.constructor.name
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = new Error(message).stack
}
}
}
@autobind
class API extends EventEmitter {
static urls = {
normal: "https://dar.kss.us.com/api",
test: "https://dar-test.kss.us.com/api",
local: `http://${config.localIPAddr || "localhost"}:3001`,
}
constructor() {
super()
this.user = { pending: true }
this._apiURL = null
const checkForToken = () => {
AsyncStorage.getItem(authTokenKeyName)
.then((token) => {
if (!token) {
return Promise.reject()
}
this.token = token
return this.who()
})
.then((user) => {
this.user = user
this.connectSocket()
this.emit("login")
})
.catch(() => {
AsyncStorage.removeItem(authTokenKeyName)
this.token = null
this.user = {}
this.socket = null
this.emit("logout")
})
}
AsyncStorage.getItem(backendKeyName)
.then((backend) => {
this._backend = backend
this.apiURL = API.urls[backend]
if (!this.apiURL) {
return Promise.reject()
}
checkForToken()
})
.catch(() => {
this._backend = "normal"
this.apiURL = API.urls[this._backend]
AsyncStorage.setItem(backendKeyName, this._backend)
checkForToken()
})
}
connectSocket() {
this.socket = io(this._baseURL, {
path: this.apiPath + "/socketio",
query: {
auth_token: this.token,
},
})
this.socket.on("disconnect", (reason) => {
// Could happen if the auth_token is bad
this.socket = null
})
this.socket.on("notify", (message) => {
const { eventName, eventData } = message
// Filter the few massages that affect our cached user data to avoid a server round trip
switch (eventName) {
default:
// Nothing to see here...
break
}
this.emit(message.eventName, message.eventData)
})
}
disconnectSocket() {
if (this.socket) {
this.socket.disconnect()
this.socket = null
}
}
get loggedInUser() {
return this.user
}
get apiURL() {
return this._apiURL
}
set apiURL(url) {
if (url) {
const parts = url.split("/")
if (parts.length < 2) {
throw new Error("Invalid API URL")
}
this._apiURL = url
this._baseURL = parts[0] + "//" + parts[1]
this._secure = parts[0] === "https:"
if (parts.length === 3) {
this._apiPath = "/" + parts[2]
} else {
this._apiPath = ""
}
}
}
get baseURL() {
return this._baseURL
}
get apiPath() {
return this._apiPath
}
get secure() {
return this._secure
}
get backend() {
return this._backend
}
set backend(backend) {
if (this._backend !== backend) {
const newBaseURL = API.urls[backend]
if (newBaseURL) {
this.apiURL = newBaseURL
this._backend = backend
AsyncStorage.setItem(backendKeyName, this._backend).then(
this.logout,
this.logout
)
}
}
}
makeImageUrl(id, size) {
if (id) {
return this.apiPath + "/assets/" + id + "?access_token=" + this.token
} else if (size && size.width && size.height) {
return `${this.apiPath}/placeholders/${size.width}x${
size.height
}?access_token=${this.token}`
} else {
return null
}
}
makeAssetUrl(id) {
return id
? this.apiPath + "/assets/" + id + "?access_token=" + this.token
: null
}
static makeParams(params) {
return params
? "?" +
Object.keys(params)
.map((key) => [key, params[key]].map(encodeURIComponent).join("="))
.join("&")
: ""
}
request(method, path, requestBody, requestOptions) {
requestOptions = requestOptions || {}
var promise = new Promise((resolve, reject) => {
let fetchOptions = {
method: method,
mode: "cors",
cache: "no-store",
}
let headers = new Headers()
if (this.token) {
headers.set("Authorization", "Bearer " + this.token)
}
if (method === "POST" || method === "PUT") {
if (requestOptions.binary) {
headers.set("Content-Type", "application/octet-stream")
headers.set("Content-Length", requestOptions.binary.length)
headers.set("Range", "byte " + requestOptions.binary.offset)
fetchOptions.body = requestBody
} else {
headers.set("Content-Type", "application/json")
fetchOptions.body = JSON.stringify(requestBody)
}
}
fetchOptions.headers = headers
if (!this.apiURL) {
return reject(new Error("API layer not ready"))
}
fetch(this.apiURL + path, fetchOptions)
.then((res) => {
return Promise.all([
Promise.resolve(res),
requestOptions.binary && method === "GET" ? res.blob() : res.json(),
])
})
.then((arr) => {
let [res, responseBody] = arr
if (res.ok) {
if (requestOptions.wantHeaders) {
resolve({ body: responseBody, headers: res.headers })
} else {
resolve(responseBody)
}
} else {
reject(new APIError(res.status, responseBody.message))
}
})
.catch((error) => {
reject(new NetworkError(error.message))
})
})
return promise
}
post(path, requestBody, options) {
return this.request("POST", path, requestBody, options)
}
put(path, requestBody, options) {
return this.request("PUT", path, requestBody, options)
}
get(path, options) {
return this.request("GET", path, options)
}
delete(path, options) {
return this.request("DELETE", path, options)
}
login(email, password, remember) {
return new Promise((resolve, reject) => {
this.post("/auth/login", { email, password }, { wantHeaders: true })
.then((response) => {
// Save bearer token for later use
const authValue = response.headers.get("Authorization")
const [scheme, token] = authValue.split(" ")
if (scheme !== "Bearer" || !token) {
reject(new APIError("Unexpected Authorization scheme or token"))
}
if (remember) {
AsyncStorage.setItem(authTokenKeyName, token)
}
this.token = token
this.user = response.body
this.connectSocket()
this.emit("login")
resolve(response.body)
})
.catch((err) => {
reject(err)
})
})
}
logout() {
let cb = () => {
// Regardless of response, always logout in the client
AsyncStorage.removeItem(authTokenKeyName)
this.token = null
this.user = {}
this.disconnectSocket()
this.emit("logout")
}
return this.delete("/auth/login").then(cb, cb)
}
who() {
return this.get("/auth/who")
}
getUser(_id) {
return this.get("/users/" + _id)
}
listUsers() {
return this.get("/users")
}
createUser(user) {
return this.post("/users", user)
}
updateUser(user) {
return new Promise((resolve, reject) => {
this.put("/users", user)
.then((user) => {
// If we just updated ourselves, update the internal cached copy
if (user._id === this.user._id) {
this.user = user
this.emit("login")
}
resolve(user)
})
.catch((reason) => {
reject(reason)
})
})
}
deleteUser(_id) {
return this.delete("/users/" + _id)
}
enterRoom(roomName) {
return this.put("/users/enter-room/" + (roomName || ""))
}
leaveRoom() {
return this.put("/users/leave-room")
}
getWorkItem(_id) {
return this.get("/workitems/" + _id)
}
listWorkItems() {
return this.get("/workitems")
}
listWorkItemActivities() {
return this.get("/workitems/activities")
}
createWorkItem(workItem) {
return this.post("/workitems", workItem)
}
updateWorkItem(workItem) {
return this.put("/workitems", workItem)
}
deleteWorkItem(_id) {
return this.delete("/workitems/" + _id)
}
getActivity(_id) {
return this.get("/activities/" + _id)
}
listActivities() {
return this.get("/activities")
}
createActivity(activity) {
return this.post("/activities", activity)
}
updateActivity(activity) {
return this.put("/activities", activity)
}
deleteActivity(_id) {
return this.delete("/activities/" + _id)
}
getAddress(coord) {
var promise = new Promise((resolve, reject) => {
let fetchOptions = {
method: "GET",
mode: "no-cors",
}
let headers = new Headers()
headers.set("Referer", config.refererURL)
fetchOptions.headers = headers
const path =
config.googleGeocodeURL +
`?latlng=${coord.latitude},${coord.longitude}&key=${
config.googleGeocodeAPIKey
}`
fetch(path, fetchOptions)
.then((res) => {
return Promise.all([Promise.resolve(res), res.json()])
})
.then((arr) => {
let [res, responseBody] = arr
if (res.ok) {
let address = ""
if (responseBody.results && responseBody.results.length > 0) {
address = responseBody.results[0].formatted_address
}
resolve(address)
} else {
reject(new APIError(res.status, responseBody.message))
}
})
.catch((error) => {
reject(new NetworkError(error.message))
})
})
return promise
}
upload(file, progressCallback) {
return new Promise((resolve, reject) => {
const chunkSize = 32 * 1024
let reader = new FileReader()
const fileSize = file.size
const numberOfChunks = Math.ceil(fileSize / chunkSize)
let chunk = 0
let uploadId = null
reader.onload = (e) => {
const buffer = e.target.result
const bytesRead = buffer.byteLength
this.post("/assets/upload/" + uploadId, buffer, {
binary: { offset: chunk * chunkSize, length: bytesRead },
})
.then((uploadData) => {
chunk++
if (!progressCallback(uploadData)) {
return Promise.reject(new Error("Upload was canceled"))
}
if (chunk < numberOfChunks) {
let start = chunk * chunkSize
let end = Math.min(fileSize, start + chunkSize)
reader.readAsArrayBuffer(file.slice(start, end))
} else {
resolve(uploadData)
}
})
.catch((err) => {
reject(err)
})
}
this.post("/assets/upload", {
fileName: file.name,
fileSize,
contentType: file.type,
numberOfChunks,
})
.then((uploadData) => {
uploadId = uploadData.uploadId
reader.readAsArrayBuffer(file.slice(0, chunkSize))
})
.catch((err) => {
reject(err)
})
})
}
}
export let api = new API()