6 Commits

Author SHA1 Message Date
John Lyon-Smith
587052a509 Fix admin/user login issues 2018-05-25 10:16:28 -07:00
John Lyon-Smith
a33ca57d58 Version 1.0.2-20180518.0 2018-05-18 08:49:19 -07:00
John Lyon-Smith
129d1e5d50 Fix deployment 2018-05-18 08:48:56 -07:00
John Lyon-Smith
4110db423b Fix server deps 2018-05-18 08:40:25 -07:00
John Lyon-Smith
27131dd4f5 Fix deploy for website 2018-05-18 08:36:41 -07:00
John Lyon-Smith
e0696b7f69 Version 1.0.1-20180518.0 2018-05-18 08:17:59 -07:00
33 changed files with 281 additions and 110 deletions

View File

@@ -101,8 +101,8 @@ android {
applicationId "com.deightonar"
minSdkVersion 24
targetSdkVersion 27
versionCode 3
versionName "1.0.0"
versionCode 5
versionName "1.0.2"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.deightonar"
android:versionCode="3"
android:versionName="1.0.0">
android:versionCode="5"
android:versionName="1.0.2">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

View File

@@ -19,11 +19,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>1.0.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>20180516.0</string>
<string>20180518.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>

View File

@@ -1,6 +1,6 @@
export const versionInfo = {
version: '1.0.0',
fullVersion: '1.0.0-20180516.0',
version: '1.0.2',
fullVersion: '1.0.2-20180518.0',
title: 'Deighton AR System',
copyright: '© 2018, Kingston Software Solutions.',
supportEmail: 'support@kss.us.com',

View File

@@ -1,6 +1,6 @@
{
"name": "deighton-ar",
"version": "1.0.0",
"version": "1.0.2",
"description": "Deighton AR Training System",
"main": "index.js",
"repository": {

View File

@@ -1,6 +1,7 @@
{
logDir: '',
serviceName: {
system: 'deighton-ar-test',
server: 'dar-test-server',
api: 'dar-test-api',
email: 'dar-test-email',
@@ -21,6 +22,7 @@
maxPasswordTokenAgeInHours: 3,
sendEmailDelayInSeconds: 3,
supportEmail: 'support@kss.us.com',
sendEmail: true
sendEmail: true,
appName: "Deighton AR Test",
}
}

View File

@@ -1,6 +1,7 @@
{
logDir: '',
serviceName: {
system: 'deighton-ar',
server: 'dar-server',
api: 'dar-api',
email: 'dar-email',
@@ -21,6 +22,7 @@
maxPasswordTokenAgeInHours: 3,
sendEmailDelayInSeconds: 3,
supportEmail: 'support@kss.us.com',
sendEmail: true
sendEmail: true,
appName: "Deighton AR",
}
}

View File

@@ -5987,6 +5987,11 @@
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
"dev": true
},
"sleep-promise": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-6.0.0.tgz",
"integrity": "sha1-qAtMPCbtVTxzydtyNLwDBPfULUo="
},
"sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",

View File

@@ -7,7 +7,7 @@
"start": "babel-node src/server.js",
"start:prod": "NODE_ENV=production node dist/server.js",
"build": "rm -rf dist && babel src -d dist -s",
"deploy": "rsync -vr -e ssh --exclude-from .rsync-exclude * $SNAP_DEPLOY_USER@$SNAP_DEPLOY_HOST:deighton-ar-server/ && ssh $SNAP_DEPLOY_USER@$SNAP_DEPLOYHOST 'cd deighton-ar-server && npm install & sudo ./ops restart & sudo ./ops --test restart'",
"deploy": "rsync -vr -e ssh --exclude-from .rsync-exclude * $SNAP_DEPLOY_USER@$SNAP_DEPLOY_HOST:deighton-ar-server/ && ssh $SNAP_DEPLOY_USER@$SNAP_DEPLOY_HOST 'cd deighton-ar-server; npm install; sudo ./ops restart; sudo ./ops --test restart'",
"test": "jest",
"actor:api": "monzilla 'src/api/**/*.js:src/database/**/*.js' -- babel-node src/api/index.js",
"actor:api:debug": "babel-node --inspect-brk src/api/index.js",
@@ -48,6 +48,7 @@
"redis": "^2.7.1",
"redis-rstream": "^0.1.3",
"regexp-pattern": "^1.0.4",
"sleep-promise": "^6.0.0",
"socket.io": "^2.0.3",
"tmp-promise": "^1.0.4",
"urlsafe-base64": "^1.0.0",

View File

@@ -8,22 +8,7 @@ import config from "config"
import autobind from "autobind-decorator"
import { PassThrough } from "stream"
import { catchAll } from "."
function pipeToGridFS(readable, writeable) {
const promise = new Promise((resolve, reject) => {
readable.on("error", (error) => {
reject(error)
})
writeable.on("error", (error) => {
reject(error)
})
writeable.on("finish", (file) => {
resolve(file)
})
})
readable.pipe(writeable)
return promise
}
import { pipeToPromise } from "../../util"
@autobind
export class AssetRoutes {
@@ -109,7 +94,7 @@ export class AssetRoutes {
}
async beginAssetUpload(req, res, next) {
const uploadId = this.db.newObjectId()
const uploadId = this.db.newObjectId().toString()
let {
fileName,
uploadSize,
@@ -224,12 +209,12 @@ export class AssetRoutes {
if (uploadedChunks >= uploadData.numberOfChunks) {
let readable = redisReadStream(this.rs.client, uploadDataId)
let writeable = this.db.gridfs.openUploadStreamWithId(
uploadId,
this.db.newObjectId(uploadId),
uploadData.fileName,
{ contentType: uploadData.contentType }
)
const file = await pipeToGridFS(readable, writeable)
const file = await pipeToPromise(readable, writeable)
await Promise.all([
this.rs.del(uploadId),

View File

@@ -25,6 +25,8 @@ export class AuthRoutes {
this.sendEmailDelayInSeconds = config.get("email.sendEmailDelayInSeconds")
this.supportEmail = config.get("email.supportEmail")
this.sendEmail = config.get("email.sendEmail")
this.appName = config.get("email.appName")
this.emailServiceName = config.get("serviceName.email")
app
.route("/auth/login")
// Used to login. Email must be confirmed.
@@ -208,11 +210,12 @@ export class AuthRoutes {
siteUrl.host
}/confirm-email?email-token%3D${savedUser.emailToken.value}`,
supportEmail: this.supportEmail,
appName: this.appName,
},
})
if (this.sendEmail) {
await this.mq.request("dar-email", "sendEmail", msgs)
await this.mq.request(this.emailServiceName, "sendEmail", msgs)
}
res.json({})
@@ -393,10 +396,11 @@ export class AuthRoutes {
siteUrl.host
}/reset-password?password-token%3D${savedUser.passwordToken.value}`,
supportEmail: this.supportEmail,
appName: this.appName,
},
}
if (this.sendEmail) {
await this.mq.request("dar-email", "sendEmail", msg)
await this.mq.request(this.emailServiceName, "sendEmail", msg)
}
res.json({})

View File

@@ -19,6 +19,8 @@ export class UserRoutes {
this.ws = container.ws
this.maxEmailTokenAgeInHours = config.get("email.maxEmailTokenAgeInHours")
this.sendEmail = config.get("email.sendEmail")
this.emailServiceName = config.get("serviceName.email")
app
.route("/users")
.get(
@@ -164,7 +166,7 @@ export class UserRoutes {
res.json(savedUser.toClient())
if (this.sendEmail) {
await this.mq.request("dar-email", "sendEmail", msg)
await this.mq.request(this.emailServiceName, "sendEmail", msg)
}
}
@@ -244,7 +246,7 @@ export class UserRoutes {
res.json({})
if (this.sendEmail) {
await this.mq.request("dar-email", "sendEmail", msg)
await this.mq.request(this.emailServiceName, "sendEmail", msg)
}
}
}

39
server/src/util.js Normal file
View File

@@ -0,0 +1,39 @@
import stream from "stream"
export function streamToBuffer(readable) {
return new Promise((resolve, reject) => {
var chunks = []
var writeable = new stream.Writable()
writeable._write = function(chunk, enc, done) {
chunks.push(chunk)
done()
}
readable.on("end", function() {
resolve(Buffer.concat(chunks))
})
readable.on("error", (err) => {
reject(err)
})
readable.pipe(writeable)
})
}
export function pipeToPromise(readable, writeable) {
const promise = new Promise((resolve, reject) => {
readable.on("error", (error) => {
reject(error)
})
writeable.on("error", (error) => {
reject(error)
})
writeable.on("finish", (file) => {
resolve(file)
})
})
readable.pipe(writeable)
return promise
}

View File

@@ -1,6 +1,6 @@
export const versionInfo = {
version: '1.0.0',
fullVersion: '1.0.0-20180516.0',
version: '1.0.2',
fullVersion: '1.0.2-20180518.0',
title: 'Deighton AR System',
copyright: '© 2018, Kingston Software Solutions.',
supportEmail: 'support@kss.us.com',

View File

@@ -16,10 +16,10 @@
tags: {
major: 1,
minor: 0,
patch: 0,
build: 20180516,
patch: 2,
build: 20180518,
revision: 0,
sequence: 3,
sequence: 5,
tz: "America/Los_Angeles",
title: "Deighton AR System",
copyright: "© 2018, Kingston Software Solutions.",

View File

@@ -62,7 +62,7 @@
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"deploy": "rsync -vr -e ssh build/* ubuntu@gs-1:deighton-ar-website/",
"deploy": "rsync -vr -e ssh build/* $SNAP_DEPLOY_USER@$SNAP_DEPLOY_HOST:deighton-ar-website/",
"lint": "eslint --ext .js --ext .jsx src/",
"lint:fix": "eslint --ext .js --ext .jsx --fix src/"
},

View File

@@ -52,12 +52,12 @@ class API extends EventEmitter {
localStorage.removeItem(authTokenKeyName)
sessionStorage.removeItem(authTokenKeyName)
this._token = null
this._user = {}
this._user = { loggedOut: true }
this.socket = null
this.emit("logout")
})
} else {
this._user = {}
this._user = { loggedOut: true }
}
}
@@ -229,7 +229,7 @@ class API extends EventEmitter {
localStorage.removeItem(authTokenKeyName)
sessionStorage.removeItem(authTokenKeyName)
this._token = null
this._user = {}
this._user = { loggedOut: true }
this.disconnectSocket()
this.emit("logout")
}

View File

@@ -2,6 +2,7 @@ import React, { Component } from "react"
import {
Login,
Logout,
Parking,
ResetPassword,
ForgotPassword,
ConfirmEmail,
@@ -29,18 +30,38 @@ export class App extends Component {
<BrowserRouter>
<Column minHeight="100vh">
<Route
path="/app"
path="/admin"
render={(props) => (
<Column.Item height={sizeInfo.headerHeight}>
<Header
{...props}
left={[
{ image: require("images/badge.png"), path: "/app/home" },
{ text: "Teams", path: "/app/teams" },
{ text: "Users", path: "/app/users" },
{ image: require("images/badge.png"), path: "/admin/home" },
{ text: "Teams", path: "/admin/teams" },
{ text: "Users", path: "/admin/users" },
]}
right={[
{ icon: "profile", path: "/app/profile" },
{ icon: "profile", path: "/admin/profile" },
{ icon: "logout", path: "/logout" },
]}
/>
</Column.Item>
)}
/>
<Route
path="/user"
render={(props) => (
<Column.Item height={sizeInfo.headerHeight}>
<Header
{...props}
left={[
{
image: require("images/badge.png"),
path: "/user/profile",
},
]}
right={[
{ icon: "profile", path: "/user/profile" },
{ icon: "logout", path: "/logout" },
]}
/>
@@ -50,18 +71,30 @@ export class App extends Component {
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/logout" component={Logout} />
<Route exact path="/parking" component={Parking} />
<Route exact path="/confirm-email" component={ConfirmEmail} />
<Route exact path="/reset-password" component={ResetPassword} />
<Route exact path="/forgot-password" component={ForgotPassword} />
<ProtectedRoute exact path="/app/profile" component={Profile} />
<ProtectedRoute exact admin path="/app/home" component={Home} />
<ProtectedRoute exact admin path="/app/teams" component={Teams} />
<ProtectedRoute exact admin path="/app/system" component={System} />
<ProtectedRoute exact admin path="/app/users" component={Users} />
<DefaultRoute redirect="/app/home" />
<ProtectedRoute exact path="/user/profile" component={Profile} />
<ProtectedRoute
exact
admin
path="/admin/profile"
component={Profile}
/>
<ProtectedRoute exact admin path="/admin/home" component={Home} />
<ProtectedRoute exact admin path="/admin/teams" component={Teams} />
<ProtectedRoute
exact
admin
path="/admin/system"
component={System}
/>
<ProtectedRoute exact admin path="/admin/users" component={Users} />
<DefaultRoute user="/user/profile" admin="/admin/home" />
</Switch>
<Route
path="/app"
path="/(user|admin)"
render={() => (
<Column.Item>
<Footer

View File

@@ -79,7 +79,7 @@ export class ConfirmEmail extends React.Component {
return (
<div>
<WaitModal
active={!!waitModal}
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>

View File

@@ -1,14 +1,49 @@
import React, { Component } from "react"
import { Route, Redirect } from "react-router-dom"
import PropTypes from "prop-types"
import { api } from "src/API"
export class DefaultRoute extends Component {
static propTypes = {
redirect: PropTypes.string,
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
}),
user: PropTypes.string.isRequired,
admin: PropTypes.string.isRequired,
}
render() {
// NOTE: When working on the site, Redirect to the page you are working on
return <Route render={() => <Redirect to={this.props.redirect} />} />
const user = api.loggedInUser
if (user.loggedOut) {
return <Route render={() => <Redirect to="/login" />} />
} else if (user.pending) {
// If login token has not yet been confirmed, park until it is then come back here
return (
<Route
render={() => (
<Redirect
to={`/parking?redirect=${this.props.location.pathname}`}
/>
)}
/>
)
} else {
// Render a redirect to the user or admin default page
return (
<Route
render={() => (
<Redirect
to={
user._id && user.administrator
? this.props.admin
: this.props.user
}
/>
)}
/>
)
}
}
}

View File

@@ -151,7 +151,7 @@ export class ForgotPassword extends Component {
</Column.Item>
<Column.Item grow>
<WaitModal
active={!!waitModal}
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>

View File

@@ -75,7 +75,9 @@ export class Login extends Component {
this.setState({ waitModal: false })
if (this.props.history) {
let url =
new URLSearchParams(window.location.search).get("redirect") || "/"
new URLSearchParams(this.props.history.location.search).get(
"redirect"
) || "/"
try {
this.props.history.replace(url)
@@ -224,7 +226,7 @@ export class Login extends Component {
</Row>
</Column.Item>
<Column.Item grow>
<WaitModal active={waitModal} message="Logging in..." />
<WaitModal open={waitModal} message="Logging in..." />
<MessageModal
error
open={!!messageModal}

View File

@@ -0,0 +1,53 @@
import React, { Component, Fragment } from "react"
import PropTypes from "prop-types"
import { api } from "src/API"
import { WaitModal } from "../Modal"
import { Column } from "ui"
import autobind from "autobind-decorator"
export class Parking extends Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
componentDidMount() {
api.addListener("login", this.goToRedirect)
api.addListener("logout", this.goToLogin)
}
componentWillUnmount() {
api.removeListener("login", this.goToRedirect)
api.removeListener("logout", this.goToLogin)
}
@autobind
goToRedirect() {
if (this.props.history) {
let url =
new URLSearchParams(this.props.history.location.search).get(
"redirect"
) || "/"
try {
this.props.history.replace(url)
} catch (error) {
this.props.history.replace("/")
}
}
}
@autobind
goToLogin() {
this.props.history.replace("/login")
}
render() {
return (
<Fragment>
<Column.Item grow>
<WaitModal open loader={false} message="Authenticating..." />
</Column.Item>
</Fragment>
)
}
}

View File

@@ -1,8 +1,7 @@
import React from 'react'
import { Route, Redirect } from 'react-router'
import { PropTypes } from 'prop-types'
import { api } from 'src/API'
import autobind from 'autobind-decorator'
import React from "react"
import { Route, Redirect } from "react-router"
import { PropTypes } from "prop-types"
import { api } from "src/API"
export class ProtectedRoute extends React.Component {
static propTypes = {
@@ -13,33 +12,32 @@ export class ProtectedRoute extends React.Component {
admin: PropTypes.bool,
}
@autobind
updateComponent() {
this.forceUpdate()
}
componentDidMount() {
api.addListener("login", this.updateComponent)
api.addListener("logout", this.updateComponent)
}
componentWillUnmount() {
api.removeListener("login", this.updateComponent)
api.removeListener("logout", this.updateComponent)
}
render(props) {
const user = api.loggedInUser
if (user.pending) {
return null
// If login token has not yet been confirmed, park until it is and redirect back here
return (
<Route
render={() => (
<Redirect
to={`/parking?redirect=${this.props.location.pathname}`}
/>
)}
/>
)
} else {
// If we are not a user or an admin go to the login page
if (!user._id || (this.props.admin && !user.administrator)) {
return (
<Redirect
to={`/login?redirect=${this.props.location.pathname}${
this.props.location.search
}`}
<Route
render={() => (
<Redirect
to={`/login?redirect=${this.props.location.pathname}${
this.props.location.search
}`}
/>
)}
/>
)
} else {

View File

@@ -205,7 +205,7 @@ export class ResetPassword extends Component {
/>
<WaitModal
active={!!waitModal}
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
</Column.Item>

View File

@@ -1,7 +1,8 @@
export { Login } from './Login'
export { Logout } from './Logout'
export { ResetPassword } from './ResetPassword'
export { ForgotPassword } from './ForgotPassword'
export { ConfirmEmail } from './ConfirmEmail'
export { DefaultRoute } from './DefaultRoute'
export { ProtectedRoute } from './ProtectedRoute'
export { Login } from "./Login"
export { Logout } from "./Logout"
export { Parking } from "./Parking"
export { ResetPassword } from "./ResetPassword"
export { ForgotPassword } from "./ForgotPassword"
export { ConfirmEmail } from "./ConfirmEmail"
export { DefaultRoute } from "./DefaultRoute"
export { ProtectedRoute } from "./ProtectedRoute"

View File

@@ -19,7 +19,7 @@ export class Home extends Component {
<PanelButton
icon="users"
text="Users"
onClick={() => this.props.history.push("/app/users")}
onClick={() => this.props.history.push("/admin/users")}
/>
</Row.Item>
<Row.Item width={sizeInfo.panelButtonSpacing} />
@@ -27,7 +27,7 @@ export class Home extends Component {
<PanelButton
icon="teams"
text="Teams"
onClick={() => this.props.history.push("/app/teams")}
onClick={() => this.props.history.push("/admin/teams")}
/>
</Row.Item>
<Row.Item width={sizeInfo.panelButtonSpacing} />
@@ -35,7 +35,7 @@ export class Home extends Component {
<PanelButton
icon="system"
text="System"
onClick={() => this.props.history.push("/app/system")}
onClick={() => this.props.history.push("/admin/system")}
/>
</Row.Item>
<Row.Item grow />

View File

@@ -352,7 +352,7 @@ export class MasterDetail extends Component {
/>
<WaitModal
active={!!waitModal}
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>

View File

@@ -47,7 +47,7 @@ export class MasterList extends React.Component {
<List.Item
key={item._id || "0"}
onClick={(e) => this.props.onItemListClick(e, index)}
active={item === this.props.selectedItem}>
open={item === this.props.selectedItem}>
<List.Icon name={data.icon} size={sizeInfo.listIcon} />
<List.Text>{data.text}</List.Text>
{item === selectedItem && selectionModified ? (

View File

@@ -4,17 +4,26 @@ import { Dimmer, Loader, Text } from 'ui'
export class WaitModal extends React.Component {
static propTypes = {
active: PropTypes.bool.isRequired,
open: PropTypes.bool.isRequired,
message: PropTypes.string,
loader: PropTypes.bool,
}
static defaultProps = {
loader: true,
}
render() {
const { active, message } = this.props
const { open, message, loader } = this.props
return (
<Dimmer active={active}>
<Loader />
{message && <Text size='huge' color='inverse'>{message}</Text>}
<Dimmer active={open}>
{loader && <Loader />}
{message && (
<Text size="huge" color="inverse">
{message}
</Text>
)}
</Dimmer>
)
}

View File

@@ -166,7 +166,7 @@ export class Profile extends Component {
/>
<WaitModal
active={!!waitModal}
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>

View File

@@ -208,7 +208,7 @@ export class System extends Component {
/>
<WaitModal
active={!!waitModal}
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
</Column.Item>

View File

@@ -1,6 +1,6 @@
export const versionInfo = {
version: '1.0.0',
fullVersion: '1.0.0-20180516.0',
version: '1.0.2',
fullVersion: '1.0.2-20180518.0',
title: 'Deighton AR System',
copyright: '© 2018, Kingston Software Solutions.',
supportEmail: 'support@kss.us.com',