Working login on mobile
This commit is contained in:
@@ -9,4 +9,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { AppRegistry, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
let styles = {
|
||||
|
||||
}
|
||||
|
||||
export class LoginPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
userName: 'john@lyon-smith.org',
|
||||
password: 'test123!'
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<KeyboardAvoidingView className="view" behavior="padding">
|
||||
<ScrollView>
|
||||
<View className="logoBox">
|
||||
<Image className="image" source={acmLogo}/>
|
||||
</View>
|
||||
<Text className="username">User Name:</Text>
|
||||
<View className="fieldBlock">
|
||||
<IconInput width={250} rounded={true} icon={iconEmail} onChange={value => this.setState({email: value})}
|
||||
value={this.state.email} iconStyle={{width: 20, height: 24, top: 12}}/>
|
||||
</View>
|
||||
<View className="fieldBlock">
|
||||
<IconInput
|
||||
password={true}
|
||||
rounded={true}
|
||||
width={250}
|
||||
icon={iconPassword}
|
||||
iconStyle={{left: 12, top: 8}}
|
||||
onChange={value => this.setState({password: value})}
|
||||
value={this.state.password}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="nextBlock">
|
||||
<NextButton busy={auth.authInProgress} width={250} title="Log In" onPress={() => this.login()}/>
|
||||
</View>
|
||||
|
||||
{errorMessage}
|
||||
|
||||
<TouchableOpacity onPress={() => this.toggleLoginData()}>
|
||||
<View className="linksBlock">
|
||||
<Text className="linksText">Sign Up</Text>
|
||||
<Text className="linksTextRight">Forgot password?</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
}
|
||||
9779
mobile/package-lock.json
generated
Normal file
9779
mobile/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
||||
"react-test-renderer": "16.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"bundler": "react-native start",
|
||||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"test": "node node_modules/jest/bin/jest.js"
|
||||
@@ -17,8 +17,13 @@
|
||||
"preset": "react-native"
|
||||
},
|
||||
"dependencies": {
|
||||
"auto-bind2": "^1.0.3",
|
||||
"eventemitter3": "^3.0.1",
|
||||
"npm": "^5.7.1",
|
||||
"react": "16.2.0",
|
||||
"react-form-binder": "^1.2.0",
|
||||
"react-native": "0.52.0",
|
||||
"react-native-navigation": "^1.1.398"
|
||||
"react-native-navigation": "^1.1.398",
|
||||
"socket.io-client": "^2.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
300
mobile/src/API.js
Normal file
300
mobile/src/API.js
Normal 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()
|
||||
31
mobile/src/screens/Error.js
Normal file
31
mobile/src/screens/Error.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, Image, Button } from 'react-native';
|
||||
import { reactAutoBind } from 'auto-bind2'
|
||||
|
||||
export class Error extends React.Component {
|
||||
static navigatorStyle = {
|
||||
navBarHidden: true,
|
||||
}
|
||||
|
||||
static styles = StyleSheet.create({
|
||||
page: {
|
||||
minHeight: '50%',
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
reactAutoBind(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={Login.styles.page}>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,63 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { api } from '../API'
|
||||
|
||||
export class Home extends React.Component {
|
||||
static navigatorButtons = {
|
||||
rightButtons: [
|
||||
{
|
||||
icon: require('./images/ar-glases.png'),
|
||||
id: 'arview',
|
||||
}
|
||||
],
|
||||
leftButtons: [
|
||||
{
|
||||
icon: require('./images/logout.png'),
|
||||
id: 'logout',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
static styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
|
||||
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this))
|
||||
}
|
||||
|
||||
onNavigatorEvent(event) {
|
||||
switch(event.id) {
|
||||
switch (event.id) {
|
||||
case 'logout':
|
||||
api.logout().then(() => {
|
||||
this.props.navigator.showModal({ screen: 'app.Login' })
|
||||
})
|
||||
case 'willAppear':
|
||||
break;
|
||||
break
|
||||
case 'didAppear':
|
||||
this.props.navigator.showModal({ screen: 'app.Login' })
|
||||
if (!api.loggedInUser) {
|
||||
this.props.navigator.showModal({ screen: 'app.Login' })
|
||||
}
|
||||
break;
|
||||
case 'willDisappear':
|
||||
break;
|
||||
break
|
||||
case 'didDisappear':
|
||||
break;
|
||||
break
|
||||
case 'willCommitPreview':
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Hello John</Text>
|
||||
<Text>Changes you make will automatically reload.</Text>
|
||||
<Text>Shake your phone to open the developer menu.</Text>
|
||||
<View style={Home.styles.container}>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,36 +1,108 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, Image, Switch, TextInput, View, Button } from 'react-native';
|
||||
import { StyleSheet, Text, Image, Switch, TextInput, KeyboardAvoidingView, ScrollView, View, Button, Alert, InteractionManager } from 'react-native';
|
||||
import logoImage from './images/deighton.png'
|
||||
import { FormBinder } from 'react-form-binder'
|
||||
import { api } from '../API'
|
||||
import { BoundSwitch, BoundInput, BoundButton } from '../ui'
|
||||
import { reactAutoBind } from 'auto-bind2'
|
||||
|
||||
export class Login extends React.Component {
|
||||
static navigatorStyle = {
|
||||
navBarHidden: true,
|
||||
}
|
||||
|
||||
static bindings = {
|
||||
email: {
|
||||
alwaysGet: true,
|
||||
isValid: (r, v) => (v !== '')
|
||||
},
|
||||
password: {
|
||||
alwaysGet: true,
|
||||
isValid: (r, v) => (v !== '')
|
||||
},
|
||||
rememberMe: {
|
||||
alwaysGet: true,
|
||||
initValue: true,
|
||||
isValid: true
|
||||
},
|
||||
login: {
|
||||
noValue: true,
|
||||
isDisabled: (r) => (!(r.anyModified && r.allValid))
|
||||
}
|
||||
}
|
||||
|
||||
static styles = StyleSheet.create({
|
||||
page: {
|
||||
minHeight: '50%',
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
logo: {
|
||||
width: '50%'
|
||||
},
|
||||
inputRow: {
|
||||
paddingTop: 10,
|
||||
width: '60%',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
switchRow: {
|
||||
paddingTop: 10,
|
||||
width: '60%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center'
|
||||
},
|
||||
buttonRow: {
|
||||
paddingTop: 10,
|
||||
width: '60%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
reactAutoBind(this)
|
||||
this.state = {
|
||||
binder: new FormBinder({}, Login.bindings)
|
||||
}
|
||||
}
|
||||
|
||||
handleLogin() {
|
||||
this.props.navigator.dismissModal()
|
||||
let obj = this.state.binder.getModifiedFieldValues()
|
||||
let { navigator } = this.props
|
||||
|
||||
if (obj) {
|
||||
api.login(obj.email, obj.password, obj.rememberMe).then((user) => {
|
||||
this.props.navigator.dismissAllModals()
|
||||
}).catch((error) => {
|
||||
this.props.navigator.dismissModal()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image source={logoImage} />
|
||||
<Text>Email:</Text>
|
||||
<TextInput />
|
||||
<Text>Password:</Text>
|
||||
<TextInput />
|
||||
<Switch /><Text>Remember Me</Text>
|
||||
<Button title='Login' onPress={this.handleLogin.bind(this)} />
|
||||
</View>
|
||||
);
|
||||
<KeyboardAvoidingView style={Login.styles.page} behavior='padding'>
|
||||
<Image style={Login.styles.logo} source={logoImage} resizeMode='contain' />
|
||||
<View style={Login.styles.inputRow}>
|
||||
<BoundInput name='email' label='Email:' placeholder='name@xyz.com' message='Must enter a valid email' binder={this.state.binder} />
|
||||
</View>
|
||||
<View style={Login.styles.inputRow}>
|
||||
<BoundInput name='password' password label='Password:' message='Must supply a password' binder={this.state.binder} />
|
||||
</View>
|
||||
<View style={Login.styles.switchRow}>
|
||||
<BoundSwitch name='rememberMe' binder={this.state.binder} label='Remember Me'/>
|
||||
</View>
|
||||
<View style={Login.styles.buttonRow}>
|
||||
<BoundButton title='Login' name='login' width='40%' onPress={this.handleLogin} binder={this.state.binder} />
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#00ff00',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
BIN
mobile/src/screens/images/ar-glases.png
Normal file
BIN
mobile/src/screens/images/ar-glases.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
mobile/src/screens/images/logout.png
Normal file
BIN
mobile/src/screens/images/logout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,8 +1,10 @@
|
||||
import { Home } from './Home'
|
||||
import { Login } from './Login'
|
||||
import { Error } from './Error'
|
||||
import { Navigation } from 'react-native-navigation'
|
||||
|
||||
export function registerScreens() {
|
||||
Navigation.registerComponent('app.Home', () => Home)
|
||||
Navigation.registerComponent('app.Login', () => Login)
|
||||
Navigation.registerComponent('app.Error', () => Error)
|
||||
}
|
||||
|
||||
67
mobile/src/ui/BoundButton.js
Normal file
67
mobile/src/ui/BoundButton.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { View, Text, TouchableHighlight } from 'react-native'
|
||||
import { reactAutoBind } from 'auto-bind2'
|
||||
|
||||
export class BoundButton extends React.Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
binder: PropTypes.object.isRequired,
|
||||
submit: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
width: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
reactAutoBind(this)
|
||||
|
||||
let { name, binder } = this.props
|
||||
|
||||
binder.addListener(name, this.updateValue)
|
||||
this.state = binder.getFieldState(name)
|
||||
}
|
||||
|
||||
updateValue(e) {
|
||||
this.setState(e.state)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.binder.removeListener(this.props.name, this.updateValue)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.binder !== this.props.binder) {
|
||||
this.props.binder.removeListener(this.props.name, this.updateValue)
|
||||
nextProps.binder.addListener(nextProps.name, this.updateValue)
|
||||
this.setState(nextProps.binder.getFieldState(nextProps.name))
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, title, submit, width, onPress } = this.props
|
||||
const { visible, disabled } = this.state
|
||||
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<View style={{ flexDirection: 'column', justifyContent: 'center', paddingHorizontal: 10, height: 40, width , backgroundColor: '#E0E0E0' }}>
|
||||
<Text style={{ alignSelf: 'center', color: '#AAAAAA' }}>{title}</Text>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={{ justifyContent: 'center', paddingHorizontal: 10, height: 40, width, backgroundColor: '#3BB0FD' }}
|
||||
underlayColor='#1A72AC'>
|
||||
<Text style={{ alignSelf: 'center', color: 'black' }}>{title}</Text>
|
||||
</TouchableHighlight>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
59
mobile/src/ui/BoundInput.js
Normal file
59
mobile/src/ui/BoundInput.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { TextInput, Text, View } from 'react-native'
|
||||
import { reactAutoBind } from 'auto-bind2'
|
||||
|
||||
export class BoundInput extends React.Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
message: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
binder: PropTypes.object.isRequired,
|
||||
password: PropTypes.bool,
|
||||
placeholder: PropTypes.string
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
reactAutoBind(this)
|
||||
this.state = props.binder.getFieldState(props.name)
|
||||
}
|
||||
|
||||
handleChangeText(newText) {
|
||||
const { binder, name } = this.props
|
||||
const state = binder.getFieldState(name)
|
||||
|
||||
if (!state.readOnly && !state.disabled) {
|
||||
this.setState(binder.updateFieldValue(name, newText))
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, password, name, placeholder, message } = this.props
|
||||
const { visible, disabled, value, valid } = this.state
|
||||
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Disabled
|
||||
|
||||
return (
|
||||
<View style={{ width: '100%' }}>
|
||||
<Text style={{ color: 'black', fontSize: 14, marginBottom: 5 }}>{label}</Text>
|
||||
<TextInput style={{ width: '100%', paddingLeft: 5, paddingRight: 5, height: 40, borderColor: 'gray', borderWidth: 1, fontSize: 16 }}
|
||||
autoCapitalize='none'
|
||||
underlineColorAndroid='white'
|
||||
value={value}
|
||||
secureTextEntry={password}
|
||||
onChangeText={this.handleChangeText}
|
||||
placeholder={placeholder} />
|
||||
<Text style={{
|
||||
fontSize: 12,
|
||||
display: valid ? 'none' : 'flex',
|
||||
color: 'red',
|
||||
}}>{message}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
55
mobile/src/ui/BoundSwitch.js
Normal file
55
mobile/src/ui/BoundSwitch.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { View, Switch, Text } from 'react-native'
|
||||
import { reactAutoBind } from 'auto-bind2'
|
||||
|
||||
export class BoundSwitch extends React.Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
binder: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
reactAutoBind(this)
|
||||
this.state = props.binder.getFieldState(props.name)
|
||||
}
|
||||
|
||||
handleValueChange() {
|
||||
const { binder, name } = this.props
|
||||
const state = binder.getFieldState(name)
|
||||
|
||||
if (!state.readOnly && !state.disabled) {
|
||||
this.setState(binder.updateFieldValue(name, !state.value))
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.binder !== this.props.binder) {
|
||||
this.setState(nextProps.binder.getFieldState(nextProps.name))
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, label } = this.props
|
||||
const { visible, disabled, value } = this.state
|
||||
|
||||
return (
|
||||
<View style={{
|
||||
display: visible ? 'flex' : 'none',
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<Switch disabled={disabled} value={value} onValueChange={this.handleValueChange} />
|
||||
<Text style={{
|
||||
color: disabled ? 'gray' : 'black',
|
||||
fontSize: 16,
|
||||
paddingLeft: 8,
|
||||
alignSelf: 'center'
|
||||
}}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
3
mobile/src/ui/index.js
Normal file
3
mobile/src/ui/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { BoundSwitch } from './BoundSwitch'
|
||||
export { BoundInput } from './BoundInput'
|
||||
export { BoundButton } from './BoundButton'
|
||||
6
server/.gitignore
vendored
6
server/.gitignore
vendored
@@ -1,7 +1,3 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
dist/
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
scratch/
|
||||
|
||||
@@ -8,30 +8,56 @@ import crypto from 'crypto'
|
||||
import urlSafeBase64 from 'urlsafe-base64'
|
||||
import util from 'util'
|
||||
|
||||
const mongoUri = config.get('uri.mongo')
|
||||
import autoBind from 'auto-bind2'
|
||||
|
||||
new DB().connect(mongoUri).then((db) => {
|
||||
console.log(`Connected to MongoDB at ${mongoUri}`)
|
||||
class SendMessageTool {
|
||||
constructor(toolName, log) {
|
||||
autoBind(this)
|
||||
this.toolName = toolName
|
||||
this.log = log
|
||||
}
|
||||
|
||||
const User = db.User
|
||||
let user = new User({
|
||||
administrator: true,
|
||||
})
|
||||
user.firstName = readlineSync.question('First name? ')
|
||||
user.lastName = readlineSync.question('Last name? ')
|
||||
user.email = readlineSync.question('Email? ')
|
||||
let password = readlineSync.question('Password? ', {hideEchoBack: true})
|
||||
let cr = credential()
|
||||
async run() {
|
||||
const mongoUri = config.get('uri.mongo')
|
||||
|
||||
util.promisify(cr.hash)(password).then((json) => {
|
||||
user.passwordHash = JSON.parse(json)
|
||||
new DB().connect(mongoUri).then((db) => {
|
||||
console.log(`Connected to MongoDB at ${mongoUri}`)
|
||||
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
console.log(`User is ${user}`)
|
||||
process.exit(0)
|
||||
}).catch((error) => {
|
||||
console.log(`error: ${error.message}`)
|
||||
process.exit(-1)
|
||||
})
|
||||
const User = db.User
|
||||
let user = new User({
|
||||
administrator: true,
|
||||
})
|
||||
user.firstName = readlineSync.question('First name? ')
|
||||
user.lastName = readlineSync.question('Last name? ')
|
||||
user.email = readlineSync.question('Email? ')
|
||||
let password = readlineSync.question('Password? ', {hideEchoBack: true})
|
||||
let cr = credential()
|
||||
|
||||
util.promisify(cr.hash)(password).then((json) => {
|
||||
user.passwordHash = JSON.parse(json)
|
||||
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
console.log(`User is ${user}`)
|
||||
process.exit(0)
|
||||
}).catch((error) => {
|
||||
console.log(`error: ${error.message}`)
|
||||
process.exit(-1)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const log = {
|
||||
info: console.info,
|
||||
error: function() { console.error(chalk.red('error:', [...arguments].join(' ')))},
|
||||
warning: function() { console.error(chalk.yellow('warning:', [...arguments].join(' ')))}
|
||||
}
|
||||
|
||||
const tool = new AddUserTool('add-user', log)
|
||||
|
||||
tool.run(process.argv.slice(2)).then((exitCode) => {
|
||||
process.exit(exitCode)
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
119
server/src/bin/sendMessage.js
Normal file
119
server/src/bin/sendMessage.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import parseArgs from 'minimist'
|
||||
import amqp from 'amqplib'
|
||||
import JSON5 from 'json5'
|
||||
import fs from 'fs'
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import chalk from 'chalk'
|
||||
import autoBind from 'auto-bind2'
|
||||
|
||||
class SendMessageTool {
|
||||
constructor(toolName, log) {
|
||||
autoBind(this)
|
||||
this.toolName = toolName
|
||||
this.log = log
|
||||
}
|
||||
|
||||
async run(argv) {
|
||||
const options = {
|
||||
string: [ 'exchange', 'type' ],
|
||||
boolean: [ 'help', 'version' ],
|
||||
alias: {
|
||||
'x': 'exchange',
|
||||
't': 'type'
|
||||
}
|
||||
}
|
||||
let args = parseArgs(argv, options)
|
||||
|
||||
if (args.help) {
|
||||
this.log.info(`
|
||||
usage: tmr-message [options] <file>
|
||||
|
||||
options:
|
||||
-x --exchange <exchange> Exchange to send the message too, e.g. tmr-image
|
||||
-t --type <message-type> The type of the message content
|
||||
`)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!args.exchange) {
|
||||
this.log.error("Must specify a message exchange")
|
||||
return -1
|
||||
}
|
||||
|
||||
if (!args.type) {
|
||||
this.log.error("Must specify message type")
|
||||
return -1
|
||||
}
|
||||
|
||||
const exchangeName = args.exchange
|
||||
const filename = args._[0]
|
||||
|
||||
if (!filename) {
|
||||
this.log.error("Must specify a JSON5 message to send")
|
||||
return -1
|
||||
}
|
||||
|
||||
let msg = null
|
||||
try {
|
||||
msg = JSON5.parse(fs.readFileSync(filename))
|
||||
} catch (err) {
|
||||
this.log.error(`Could not read '${filename}'`)
|
||||
return -1
|
||||
}
|
||||
const correlationId = uuidv4()
|
||||
const replyQueueName = `reply-${uuidv4()}`
|
||||
const withChannel = async (ch) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const q = await ch.assertQueue(replyQueueName, {exclusive: true})
|
||||
|
||||
if (!q) {
|
||||
return reject(new Error(`Could not create reply queue ${replyQueueName}`))
|
||||
}
|
||||
ch.consume(q.queue, async (resMsg) => {
|
||||
this.log.info(` Response ${resMsg.content.toString()}`)
|
||||
await ch.close()
|
||||
resolve(0)
|
||||
}, {noAck: true})
|
||||
|
||||
const ok = await ch.checkExchange(exchangeName)
|
||||
|
||||
if (!ok) {
|
||||
reject(new Error(`Could not create exchange ${exchangeName}`))
|
||||
}
|
||||
|
||||
const s = JSON.stringify(msg)
|
||||
|
||||
this.log.info(` Type '${args.type}', Correlation id '${correlationId}'`)
|
||||
this.log.info(` Sent '${s}'`)
|
||||
|
||||
ch.publish(exchangeName, '', new Buffer(s), {
|
||||
type: args.type,
|
||||
contentType: 'application/json',
|
||||
timestamp: Date.now(),
|
||||
correlationId,
|
||||
appId: 'tmr-cli',
|
||||
replyTo: replyQueueName
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const conn = await amqp.connect('amqp://localhost')
|
||||
const ch = await conn.createChannel()
|
||||
|
||||
await withChannel(ch)
|
||||
}
|
||||
}
|
||||
|
||||
const log = {
|
||||
info: console.info,
|
||||
error: function() { console.error(chalk.red('error:', [...arguments].join(' ')))},
|
||||
warning: function() { console.error(chalk.yellow('warning:', [...arguments].join(' ')))}
|
||||
}
|
||||
|
||||
const tool = new SendMessageTool('send-message', log)
|
||||
|
||||
tool.run(process.argv.slice(2)).then((exitCode) => {
|
||||
process.exit(exitCode)
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
Reference in New Issue
Block a user