Working login on mobile

This commit is contained in:
John Lyon-Smith
2018-03-12 19:11:13 -07:00
parent eeb3eb4947
commit e7aaa014ff
19 changed files with 10606 additions and 131 deletions

300
mobile/src/API.js Normal file
View File

@@ -0,0 +1,300 @@
import EventEmitter from 'eventemitter3'
import io from 'socket.io-client'
import { AsyncStorage } from 'react-native'
const authTokenName = 'AuthToken'
const apiURL = 'http://localhost:3000'
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
}
}
}
class API extends EventEmitter {
constructor() {
super()
this.user = null
AsyncStorage.getItem(authTokenName).then((token) => {
if (!token) {
return Promise.reject()
}
this.token = token
this.user = { pending: true }
return this.who()
}).then((user) => {
this.user = user
this.connectSocket()
this.emit('login')
}).catch(() => {
AsyncStorage.removeItem(authTokenName)
this.token = null
this.user = null
this.socket = null
this.emit('logout')
})
}
connectSocket() {
this.socket = io(apiURL, {
path: '/api/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) {
case 'newThumbnailImage':
this.user.thumbnailImageId = eventData.imageId
break
case 'newProfileImage':
this.user.imageId = eventData.imageId
break
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
}
makeImageUrl(id, size) {
if (id) {
return '/api/assets/' + id + '?access_token=' + this.token
} else if (size && size.width && size.height) {
return `/api/placeholders/${size.width}x${size.height}?access_token=${this.token}`
} else {
return null
}
}
makeAssetUrl(id) {
return id ? '/api/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
fetch(apiURL + '/api' + 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(authTokenName, 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(authTokenName)
this.token = null
this.user = null
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)
}
setUserImage(details) {
return this.put('/users/set-image', details)
}
enterRoom(roomName) {
return this.put('/users/enter-room/' + (roomName || ''))
}
leaveRoom() {
return this.put('/users/leave-room')
}
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()