439 lines
11 KiB
JavaScript
439 lines
11 KiB
JavaScript
import EventEmitter from "eventemitter3"
|
|
import io from "socket.io-client"
|
|
import { AsyncStorage } from "react-native"
|
|
import autobind from "autobind-decorator"
|
|
import { localIPAddr } from "./development"
|
|
|
|
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://${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)
|
|
}
|
|
|
|
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()
|