diff --git a/mobile/ios/DeightonAR.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/DeightonAR.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/mobile/ios/DeightonAR.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/mobile/src/API.js b/mobile/src/API.js
index f37c49b..f199444 100644
--- a/mobile/src/API.js
+++ b/mobile/src/API.js
@@ -279,6 +279,9 @@ class API extends EventEmitter {
listWorkItems() {
return this.get("/workitems")
}
+ listWorkItemActivities() {
+ return this.get("/workitems/activities")
+ }
createWorkItem(workItem) {
return this.post("/workitems", workItem)
}
@@ -289,6 +292,22 @@ class API extends EventEmitter {
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
diff --git a/mobile/src/Home/Home.js b/mobile/src/Home/Home.js
index c1f3f83..6b207cd 100644
--- a/mobile/src/Home/Home.js
+++ b/mobile/src/Home/Home.js
@@ -3,7 +3,7 @@ import {
StyleSheet,
Text,
TextInput,
- FlatList,
+ SectionList,
Image,
View,
TouchableOpacity,
@@ -14,6 +14,7 @@ import { api } from "../API"
import autobind from "autobind-decorator"
import pinImage from "./images/pin.png"
import { ifIphoneX } from "react-native-iphone-x-helper"
+import { workItemTypeText, pad } from "../util"
const styles = StyleSheet.create({
container: {
@@ -24,68 +25,20 @@ const styles = StyleSheet.create({
},
})
-const data = [
- {
- key: "1",
- title: "Remove Animal Carcass",
- location: "Ossington Ave. | 0.2 mi.",
- state: "planned",
- latlng: { latitude: 43.653226, longitude: -79.383184 },
- },
- {
- key: "2",
- title: "Fix sign post",
- location: "Alexandre St. | 0.7 mi.",
- state: "open",
- latlng: { latitude: 43.648118, longitude: 79.392636 },
- },
- {
- key: "3",
- title: "Overflowing trash",
- location: "Bay St. | 0.8 mi.",
- state: "open",
- latlng: { latitude: 43.640168, longitude: -79.409373 },
- },
- {
- key: "4",
- title: "Leaking water pipe",
- location: "Bloor St. | 1.2 mi.",
- state: "planned",
- latlng: { latitude: 43.63311, longitude: -79.41588 },
- },
- {
- key: "5",
- title: "Tree branch in road",
- location: "Blue Jays Way | 2.2 mi.",
- state: "open",
- latlng: { latitude: 43.653526, longitude: -79.361385 },
- },
- {
- key: "6",
- title: "Washing machine on sidewalk",
- location: "Christie St. | 3.0 mi.",
- state: "open",
- latlng: { latitude: 43.66387, longitude: -79.383705 },
- },
- {
- key: "7",
- title: "Dead moose",
- location: "Cummer Ave. | 4.2 mi.",
- state: "open",
- latlng: { latitude: 43.659166, longitude: -79.39135 },
- },
- {
- key: "8",
- title: "Glass in street",
- location: "Danforth Ave. | 4.7 mi.",
- state: "open",
- latlng: { latitude: 43.663538, longitude: -79.423212 },
- },
-]
-
export class Home extends React.Component {
constructor(props) {
super(props)
+ this.state = {
+ sections: [],
+ }
+ api
+ .listWorkItemActivities()
+ .then((list) => {
+ this.setState({ sections: list.items })
+ })
+ .catch((err) => {
+ console.error(err)
+ })
}
@autobind
@@ -135,6 +88,8 @@ export class Home extends React.Component {
}
render() {
+ const { sections } = this.state
+
return (
- {data.map((marker) => (
+ {sections.map((section, index) => (
@@ -178,7 +133,7 @@ export class Home extends React.Component {
alignItems: "center",
width: "100%",
height: 40,
- backgroundColor: "#F4F4F4",
+ backgroundColor: "white",
}}>
- {
+ sections={sections}
+ renderSectionHeader={({ section: workItem }) => (
+
+
+ WORK ORDER {pad(workItem.ticketNumber, 4)}
+
+
+ )}
+ renderItem={({ item: activity, section }) => {
return (
-
+
- {item.state.toUpperCase()}
+ {activity.status.toUpperCase()}
- {item.title}
+
+ {activity.resolution}
+
- {item.location}
+ {activity.address || "..."}
this.handleItemSelect(item, index)}>
+ onPress={() => this.handleItemSelect(activity, index)}>
diff --git a/mobile/src/WorkItem/WorkItem.js b/mobile/src/WorkItem/WorkItem.js
index e71ad5c..18fc395 100644
--- a/mobile/src/WorkItem/WorkItem.js
+++ b/mobile/src/WorkItem/WorkItem.js
@@ -76,6 +76,12 @@ export class WorkItem extends React.Component {
this.state = {
binder: new FormBinder({}, WorkItem.bindings),
messageModal: null,
+ region: {
+ latitude: 43.653908,
+ longitude: -79.384293,
+ latitudeDelta: 0.0922,
+ longitudeDelta: 0.0421,
+ },
}
const { search } = this.props.location
@@ -92,6 +98,12 @@ export class WorkItem extends React.Component {
workItem.location = formatLatLng(lat, lng)
this.setState({
binder: new FormBinder(workItem, WorkItem.bindings),
+ region: {
+ latitude: lat,
+ longitude: lng,
+ latitudeDelta: 0.01,
+ longitudeDelta: 0.01,
+ },
})
}
})
@@ -179,7 +191,7 @@ export class WorkItem extends React.Component {
}
render() {
- const { binder, messageModal } = this.state
+ const { binder, messageModal, region } = this.state
return (
@@ -223,12 +235,7 @@ export class WorkItem extends React.Component {
showsTraffic={false}
showsIndoors={false}
zoomControlEnabled
- initialRegion={{
- latitude: 43.653908,
- longitude: -79.384293,
- latitudeDelta: 0.0922,
- longitudeDelta: 0.0421,
- }}
+ region={region}
onRegionChange={this.handleRegionChange}
/>
{
- result[item.value] = item.text
- return result
-}, {})
-
export class WorkItemList extends React.Component {
constructor(props) {
super(props)
diff --git a/mobile/src/util.js b/mobile/src/util.js
index a7ff605..5272d13 100644
--- a/mobile/src/util.js
+++ b/mobile/src/util.js
@@ -45,6 +45,11 @@ export const workItemTypeEnum = [
{ value: "complaint", text: "Complaint" },
]
+export const workItemTypeText = workItemTypeEnum.reduce((result, item) => {
+ result[item.value] = item.text
+ return result
+}, {})
+
export const pad = (num, size) => {
var s = num + ""
while (s.length < size) s = "0" + s
diff --git a/server/package-lock.json b/server/package-lock.json
index bad85d9..8fddfba 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1197,9 +1197,9 @@
}
},
"bson": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz",
- "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw="
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz",
+ "integrity": "sha512-D8zmlb46xfuK2gGvKmUjIklQEouN2nQ0LEHHeZ/NoHM2LDiMk2EYzZ5Ntw/Urk+bgMDosOZxaRzXxvhI5TcAVQ=="
},
"buffer": {
"version": "4.9.1",
@@ -3324,11 +3324,6 @@
"os-tmpdir": "1.0.2"
}
},
- "hooks-fixed": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.2.tgz",
- "integrity": "sha512-YurCM4gQSetcrhwEtpQHhQ4M7Zo7poNGqY4kQGeBS6eZtOcT3tnNs01ThFa0jYBByAiYt1MjMjP/YApG0EnAvQ=="
- },
"hosted-git-info": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
@@ -4424,9 +4419,9 @@
}
},
"kareem": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz",
- "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg="
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.0.6.tgz",
+ "integrity": "sha512-/C+l8gABdHsAIfNpykJNWmYodpTnDRyn+JhORkP2VgEf1GgdAc+oTHjVADwISwCJKta031EOIwY6+Hki5z8SpQ=="
},
"kind-of": {
"version": "3.2.2",
@@ -4648,12 +4643,12 @@
"integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg=="
},
"mongodb": {
- "version": "2.2.34",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.34.tgz",
- "integrity": "sha1-o09Zu+thdUrsQy3nLD/iFSakTBo=",
+ "version": "2.2.35",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.35.tgz",
+ "integrity": "sha512-3HGLucDg/8EeYMin3k+nFWChTA85hcYDCw1lPsWR6yV9A6RgKb24BkLiZ9ySZR+S0nfBjWoIUS7cyV6ceGx5Gg==",
"requires": {
"es6-promise": "3.2.1",
- "mongodb-core": "2.1.18",
+ "mongodb-core": "2.1.19",
"readable-stream": "2.2.7"
},
"dependencies": {
@@ -4687,34 +4682,56 @@
}
},
"mongodb-core": {
- "version": "2.1.18",
- "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.18.tgz",
- "integrity": "sha1-TEYTm986HwMt7ZHbSfOO7AFlkFA=",
+ "version": "2.1.19",
+ "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.19.tgz",
+ "integrity": "sha512-Jt4AtWUkpuW03kRdYGxga4O65O1UHlFfvvInslEfLlGi+zDMxbBe3J2NVmN9qPJ957Mn6Iz0UpMtV80cmxCVxw==",
"requires": {
- "bson": "1.0.4",
+ "bson": "1.0.6",
"require_optional": "1.0.1"
}
},
"mongoose": {
- "version": "4.13.11",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.13.11.tgz",
- "integrity": "sha512-OgXmFc3vzXwq4zWp41XfSBDnKZLqnBc4Kh7mwwGjBE5iWH5tfkixaPK0uFtpEuzDzUvAIg33bgniyTsmc00olA==",
+ "version": "5.0.13",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.0.13.tgz",
+ "integrity": "sha512-VCiutgdxwhTuNHIuUgMRWVYvv0GFw6FUi4j14B7um/Wcy1uhuwF552a6XVKUCth/AY8C+PjVU9fVGJ5K0JmrmQ==",
"requires": {
"async": "2.1.4",
- "bson": "1.0.4",
- "hooks-fixed": "2.0.2",
- "kareem": "1.5.0",
+ "bson": "1.0.6",
+ "kareem": "2.0.6",
"lodash.get": "4.4.2",
- "mongodb": "2.2.34",
+ "mongodb": "3.0.4",
+ "mongoose-legacy-pluralize": "1.0.2",
"mpath": "0.3.0",
- "mpromise": "0.5.5",
- "mquery": "2.3.3",
+ "mquery": "3.0.0",
"ms": "2.0.0",
- "muri": "1.3.0",
"regexp-clone": "0.0.1",
"sliced": "1.0.1"
+ },
+ "dependencies": {
+ "mongodb": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.4.tgz",
+ "integrity": "sha512-90YIIs7A4ko4kCGafxxXj3foexCAlJBC0YLwwIKgSLoE7Vni2IqUMz6HSsZ3zbXOfR1KWtxfnc0RyAMAY/ViLg==",
+ "requires": {
+ "mongodb-core": "3.0.4"
+ }
+ },
+ "mongodb-core": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.4.tgz",
+ "integrity": "sha512-OTH267FjfwBdEufSnrgd+u8HuLWRuQ6p8DR0XirPl2BdlLEMh4XwjJf1RTlruILp5p2m1w8dDC8rCxibC3W8qQ==",
+ "requires": {
+ "bson": "1.0.6",
+ "require_optional": "1.0.1"
+ }
+ }
}
},
+ "mongoose-legacy-pluralize": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
+ "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ=="
+ },
"mongoose-merge-plugin": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/mongoose-merge-plugin/-/mongoose-merge-plugin-0.0.5.tgz",
@@ -4751,15 +4768,10 @@
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz",
"integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q="
},
- "mpromise": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz",
- "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY="
- },
"mquery": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.3.tgz",
- "integrity": "sha512-NC8L14kn+qxJbbJ1gbcEMDxF0sC3sv+1cbRReXXwVvowcwY1y9KoVZFq0ebwARibsadu8lx8nWGvm3V0Pf0ZWQ==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.0.0.tgz",
+ "integrity": "sha512-WL1Lk8v4l8VFSSwN3yCzY9TXw+fKVYKn6f+w86TRzOLSE8k1yTgGaLBPUByJQi8VcLbOdnUneFV/y3Kv874pnQ==",
"requires": {
"bluebird": "3.5.0",
"debug": "2.6.9",
@@ -4784,11 +4796,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
- "muri": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz",
- "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg=="
- },
"nan": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
diff --git a/server/package.json b/server/package.json
index 56ddc4b..32a1818 100644
--- a/server/package.json
+++ b/server/package.json
@@ -34,8 +34,8 @@
"http-errors": "^1.6.1",
"json5": "^0.5.1",
"jsonwebtoken": "^7.4.0",
- "mongodb": "^2.2.31",
- "mongoose": "^4.11.7",
+ "mongodb": "^2.2.35",
+ "mongoose": "^5.0.13",
"mongoose-merge-plugin": "0.0.5",
"nodemailer": "^4.0.1",
"passport": "^0.3.2",
diff --git a/server/src/api/index.js b/server/src/api/index.js
index 983394c..9ce6637 100644
--- a/server/src/api/index.js
+++ b/server/src/api/index.js
@@ -1,31 +1,32 @@
-import config from 'config'
-import express from 'express'
-import pino from 'pino'
-import * as pinoExpress from 'pino-pretty-express'
-import http from 'http'
-import { DB } from '../database'
-import { MQ } from './MQ'
-import { RS } from './RS'
-import { WS } from './WS'
-import bodyParser from 'body-parser'
-import cors from 'cors'
-import passport from 'passport'
-import { Strategy as BearerStrategy } from 'passport-http-bearer'
-import path from 'path'
-import fs from 'fs'
-import createError from 'http-errors'
-import * as Routes from './routes'
+import config from "config"
+import express from "express"
+import pino from "pino"
+import * as pinoExpress from "pino-pretty-express"
+import http from "http"
+import { DB } from "../database"
+import { MQ } from "./MQ"
+import { RS } from "./RS"
+import { WS } from "./WS"
+import bodyParser from "body-parser"
+import cors from "cors"
+import passport from "passport"
+import { Strategy as BearerStrategy } from "passport-http-bearer"
+import path from "path"
+import fs from "fs"
+import createError from "http-errors"
+import * as Routes from "./routes"
let app = express()
let server = http.createServer(app)
let container = { app, server }
-const serviceName = 'dar-api'
-const isProduction = (process.env.NODE_ENV == 'production')
+const serviceName = "dar-api"
+const isProduction = process.env.NODE_ENV == "production"
let log = null
if (isProduction) {
- log = pino( { name: serviceName },
- fs.createWriteStream(path.join(config.get('logDir'), serviceName + '.log'))
+ log = pino(
+ { name: serviceName },
+ fs.createWriteStream(path.join(config.get("logDir"), serviceName + ".log"))
)
} else {
const pretty = pinoExpress.pretty({})
@@ -35,12 +36,12 @@ if (isProduction) {
container.log = log
app.use(pinoExpress.config({ log }))
-app.set('etag', false) // Not wanted for _all_ routes. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag.
-app.options('*', cors()) // Enable all pre-flight CORS requests
+app.set("etag", false) // Not wanted for _all_ routes. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag.
+app.options("*", cors()) // Enable all pre-flight CORS requests
app.use(cors())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
-app.use(bodyParser.raw({ type: 'application/octet-stream'})) // TODO: Support gzip, etc.. here
+app.use(bodyParser.raw({ type: "application/octet-stream" })) // TODO: Support gzip, etc.. here
app.use(passport.initialize())
const rs = new RS(container)
@@ -54,55 +55,61 @@ container.mq = mq
passport.use(new BearerStrategy(db.lookupToken))
-let mongoUri = config.get('uri.mongo')
-let amqpUri = config.get('uri.amqp')
-let redisUri = config.get('uri.redis')
+let mongoUri = config.get("uri.mongo")
+let amqpUri = config.get("uri.amqp")
+let redisUri = config.get("uri.redis")
Promise.all([
db.connect(mongoUri, isProduction),
mq.connect(amqpUri),
rs.connect(redisUri),
- ws.listen()
-]).then(() => {
- log.info(`Connected to MongoDB at ${mongoUri}`)
- log.info(`Connected to RabbitMQ at ${amqpUri}`)
- log.info(`Connected to Redis at ${redisUri}`)
+ ws.listen(),
+])
+ .then(() => {
+ log.info(`Connected to MongoDB at ${mongoUri}`)
+ log.info(`Connected to RabbitMQ at ${amqpUri}`)
+ log.info(`Connected to Redis at ${redisUri}`)
- try {
- container.authRoutes = new Routes.AuthRoutes(container)
- container.userRoutes = new Routes.UserRoutes(container)
- container.assetRoutes = new Routes.AssetRoutes(container)
- container.workItemRoutes = new Routes.WorkItemRoutes(container)
- container.teamRoutes = new Routes.TeamRoutes(container)
- container.activityRoutes = new Routes.ActivityRoutes(container)
+ try {
+ container.authRoutes = new Routes.AuthRoutes(container)
+ container.userRoutes = new Routes.UserRoutes(container)
+ container.assetRoutes = new Routes.AssetRoutes(container)
+ container.workItemRoutes = new Routes.WorkItemRoutes(container)
+ container.teamRoutes = new Routes.TeamRoutes(container)
+ container.activityRoutes = new Routes.ActivityRoutes(container)
- app.use(function(req, res, next) {
- res.status(404).json({
- message: 'Not found'
+ app.use(function(req, res, next) {
+ res.status(404).json({
+ message: "Not found",
+ })
})
- })
- app.use(function(err, req, res, next) {
- if (!isProduction) {
- log.error(err)
- }
+ app.use(function(err, req, res, next) {
+ if (!isProduction) {
+ log.error(err)
+ }
- if (!err.status) {
- err = createError.InternalServerError(err.message)
- }
+ if (!err.status) {
+ err = createError.InternalServerError(err.message)
+ }
- res.status(err.status).json({
- message: err.message
+ res.status(err.status).json({
+ message: err.message,
+ })
})
- })
- } catch(error) {
- console.error(error)
- process.exit(-1)
- }
+ } catch (error) {
+ console.error(error)
+ process.exit(-1)
+ }
- let port = config.get('api.port')
- server.listen(port)
- log.info(`Deight AR API started on port ${port}`)
-}).catch((err) => {
- log.error(err.message)
- process.exit(1)
-})
+ let port = config.get("api.port")
+ server.listen(port)
+ log.info(`Deight AR API started on port ${port}`)
+ })
+ .catch((err) => {
+ if (isProduction) {
+ log.error(err.message)
+ } else {
+ console.log(err)
+ }
+ process.exit(1)
+ })
diff --git a/server/src/api/routes/ActivityRoutes.js b/server/src/api/routes/ActivityRoutes.js
index d482aa4..a71184f 100644
--- a/server/src/api/routes/ActivityRoutes.js
+++ b/server/src/api/routes/ActivityRoutes.js
@@ -1,6 +1,7 @@
-import passport from 'passport'
-import createError from 'http-errors'
-import autobind from 'autobind-decorator'
+import passport from "passport"
+import createError from "http-errors"
+import autobind from "autobind-decorator"
+import { catchAll } from "."
@autobind
export class ActivityRoutes {
@@ -12,59 +13,72 @@ export class ActivityRoutes {
this.mq = container.mq
this.ws = container.ws
- app.route('/activities')
- .get(passport.authenticate('bearer', { session: false }), this.listActivitys)
- .post(passport.authenticate('bearer', { session: false }), this.createActivity)
- .put(passport.authenticate('bearer', { session: false }), this.updateActivity)
+ app
+ .route("/activities")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.listActivities)
+ )
+ .post(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.createActivity)
+ )
+ .put(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.updateActivity)
+ )
- app.route('/activities/:_id([a-f0-9]{24})')
- .get(passport.authenticate('bearer', { session: false }), this.getActivity)
- .delete(passport.authenticate('bearer', { session: false }), this.deleteActivity)
+ app
+ .route("/activities/:_id([a-f0-9]{24})")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.getActivity)
+ )
+ .delete(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.deleteActivity)
+ )
}
- listActivitys(req, res, next) {
+ async listActivities(req, res, next) {
const Activity = this.db.Activity
const limit = req.query.limit || 20
const skip = req.query.skip || 0
const partial = !!req.query.partial
- const branch = req.query.branch
- const query = {}
+ let query = {}
- if (branch) {
- query.branch = branch
- }
+ const total = await Activity.count({})
- Activity.count({}).then((total) => {
- let activities = []
- let cursor = Activity.find(query).limit(limit).skip(skip).cursor().map((doc) => {
+ let Activities = []
+ let cursor = Activity.find(query)
+ .limit(limit)
+ .skip(skip)
+ .cursor()
+ .map((doc) => {
return doc.toClient(partial)
})
- cursor.on('data', (doc) => {
- activities.push(doc)
+ cursor.on("data", (doc) => {
+ Activities.push(doc)
+ })
+ cursor.on("end", () => {
+ res.json({
+ total: total,
+ offset: skip,
+ count: activities.length,
+ items: activities,
})
- cursor.on('end', () => {
- res.json({
- total: total,
- offset: skip,
- count: activities.length,
- items: activities
- })
- })
- cursor.on('error', (err) => {
- next(createError.InternalServerError(err.message))
- })
- }).catch((err) => {
- next(createError.InternalServerError(err.message))
+ })
+ cursor.on("error", (err) => {
+ throw createError.InternalServerError(err.message)
})
}
- createActivity(req, res, next) {
- const role = req.user.role
+ async createActivity(req, res, next) {
+ const isAdmin = req.user.administrator
- // If user's role is not Executive or Administrator, return an error
- if (role !== 'executive' && role !== 'administrator') {
- return next(new createError.Forbidden())
+ if (!isAdmin) {
+ return new createError.Forbidden()
}
// Create a new Activity template then assign it to a value in the req.body
@@ -72,24 +86,21 @@ export class ActivityRoutes {
let activity = new Activity(req.body)
// Save the activity (with promise) - If it doesnt, catch and throw error
- activity.save().then((newActivity) => {
- res.json(newActivity.toClient())
- }).catch((err) => {
- next(createError.InternalServerError(err.message))
- })
+ const newActivity = await activity.save()
+
+ res.json(newActivity.toClient())
}
- updateActivity(req, res, next) {
- const role = req.user.role
+ async updateActivity(req, res, next) {
+ const isAdmin = req.user.administrator
- // If user's role is not Executive or Administrator, return an error
- if (role !== 'executive' && role !== 'administrator') {
+ if (!isAdmin) {
return new createError.Forbidden()
}
// Do this here because Mongoose will add it automatically otherwise
if (!req.body._id) {
- return next(createError.BadRequest('No _id given in body'))
+ throw createError.BadRequest("No _id given in body")
}
let Activity = this.db.Activity
@@ -98,56 +109,51 @@ export class ActivityRoutes {
try {
activityUpdates = new Activity(req.body)
} catch (err) {
- return next(createError.BadRequest('Invalid data'))
+ throw createError.BadRequest("Invalid data")
}
- Activity.findById(activityUpdates._id).then((foundActivity) => {
- if (!foundActivity) {
- return next(createError.NotFound(`Activity with _id ${_id} was not found`))
- }
- foundActivity.merge(activityUpdates)
- return foundActivity.save()
- }).then((savedActivity) => {
- res.json(savedActivity.toClient())
- }).catch((err) => {
- next(createError.InternalServerError(err.message))
- })
+ const foundActivity = await Activity.findById(activityUpdates._id)
+
+ if (!foundActivity) {
+ return next(
+ createError.NotFound(`Activity with _id ${_id} was not found`)
+ )
+ }
+
+ foundActivity.merge(activityUpdates)
+
+ const savedActivity = await foundActivity.save()
+
+ res.json(savedActivity.toClient())
}
- getActivity(req, res, next) {
+ async getActivity(req, res, next) {
const Activity = this.db.Activity
const _id = req.params._id
+ const activity = await Activity.findById(_id)
- Activity.findById(_id).then((activity) => {
- if (!activity) {
- return next(createError.NotFound(`Activity with _id ${_id} not found`))
- }
+ if (!activity) {
+ throw createError.NotFound(`Activity with _id ${_id} not found`)
+ }
- res.json(activity.toClient())
- }).catch((err) => {
- next(createError.InternalServerError(err.message))
- })
+ res.json(activity.toClient())
}
- deleteActivity(req, res, next) {
- const role = req.user.role
+ async deleteActivity(req, res, next) {
+ const isAdmin = req.user.administrator
- // If user's role is not Executive or Administrator, return an error
- if (role !== 'executive' && role !== 'administrator') {
+ if (!isAdmin) {
return new createError.Forbidden()
}
const Activity = this.db.Activity
const _id = req.params._id
+ const activity = await Activity.remove({ _id })
- Activity.remove({ _id }).then((activity) => {
- if (!activity) {
- return next(createError.NotFound(`Activity with _id ${_id} not found`))
- }
+ if (!activity) {
+ throw createError.NotFound(`Activity with _id ${_id} not found`)
+ }
- res.json({})
- }).catch((err) => {
- next(createError.InternalServerError(err.message))
- })
+ res.json({})
}
}
diff --git a/server/src/api/routes/AssetRoutes.js b/server/src/api/routes/AssetRoutes.js
index f9f0ac2..738f14f 100644
--- a/server/src/api/routes/AssetRoutes.js
+++ b/server/src/api/routes/AssetRoutes.js
@@ -1,21 +1,21 @@
-import passport from 'passport'
-import redis from 'redis'
-import redisReadStream from 'redis-rstream'
-import createError from 'http-errors'
-import path from 'path'
-import util from 'util'
-import config from 'config'
-import autobind from 'autobind-decorator'
+import passport from "passport"
+import redis from "redis"
+import redisReadStream from "redis-rstream"
+import createError from "http-errors"
+import path from "path"
+import util from "util"
+import config from "config"
+import autobind from "autobind-decorator"
function pipeToGridFS(readable, gfsWriteable) {
const promise = new Promise((resolve, reject) => {
- readable.on('error', (error) => {
+ readable.on("error", (error) => {
reject(error)
})
- gfsWriteable.on('error', (error) => {
+ gfsWriteable.on("error", (error) => {
reject(error)
})
- gfsWriteable.on('close', (file) => {
+ gfsWriteable.on("close", (file) => {
resolve(file)
})
})
@@ -32,55 +32,85 @@ export class AssetRoutes {
this.db = container.db
this.rs = container.rs
- this.uploadTimeout = config.get('api.uploadTimout')
- app.route('/assets/:_id')
- .get(passport.authenticate('bearer', { session: false }), this.getAsset)
- .delete(passport.authenticate('bearer', { session: false }), this.deleteAsset)
+ this.uploadTimeout = config.get("api.uploadTimout")
+ app
+ .route("/assets/:_id")
+ .get(passport.authenticate("bearer", { session: false }), this.getAsset)
+ .delete(
+ passport.authenticate("bearer", { session: false }),
+ this.deleteAsset
+ )
- app.route('/assets/upload')
- .post(passport.authenticate('bearer', { session: false }), this.beginAssetUpload)
+ app
+ .route("/assets/upload")
+ .post(
+ passport.authenticate("bearer", { session: false }),
+ this.beginAssetUpload
+ )
- app.route('/assets/upload/:_id')
- .post(passport.authenticate('bearer', { session: false }), this.continueAssetUpload)
+ app
+ .route("/assets/upload/:_id")
+ .post(
+ passport.authenticate("bearer", { session: false }),
+ this.continueAssetUpload
+ )
}
getAsset(req, res, next) {
const assetId = req.params._id
- this.db.gridfs.findOneAsync({ _id: assetId }).then((file) => {
- if (!file) {
- return next(createError.NotFound(`Asset ${assetId} was not found`))
- }
+ this.db.gridfs
+ .findOneAsync({ _id: assetId })
+ .then((file) => {
+ if (!file) {
+ return next(createError.NotFound(`Asset ${assetId} was not found`))
+ }
- const ifNoneMatch = req.get('If-None-Match')
+ const ifNoneMatch = req.get("If-None-Match")
- if (ifNoneMatch && ifNoneMatch === file.md5) {
- res.status(304).set({
- 'ETag': file.md5,
- 'Cache-Control': 'private,max-age=86400'
- }).end()
- return
- }
+ if (ifNoneMatch && ifNoneMatch === file.md5) {
+ res
+ .status(304)
+ .set({
+ ETag: file.md5,
+ "Cache-Control": "private,max-age=86400",
+ })
+ .end()
+ return
+ }
- res.status(200).set({
- 'Content-Type': file.contentType,
- 'Content-Length': file.length,
- 'ETag': file.md5})
+ res.status(200).set({
+ "Content-Type": file.contentType,
+ "Content-Length": file.length,
+ ETag: file.md5,
+ })
- this.db.gridfs.createReadStream({ _id: file._id }).pipe(res)
- }).catch((err) => {
- next(createError.BadRequest(`Error returning asset '${assetId}'. ${err.message}`))
- })
+ this.db.gridfs.createReadStream({ _id: file._id }).pipe(res)
+ })
+ .catch((err) => {
+ next(
+ createError.BadRequest(
+ `Error returning asset '${assetId}'. ${err.message}`
+ )
+ )
+ })
}
deleteAsset(req, res, next) {
const assetId = req.params._id
- this.db.gridfs.removeAsync({ _id: assetId }).then(() => {
- res.json({})
- }).catch((err) => {
- next(createError.BadRequest(`Unable to delete asset '${assetId}'. ${err.message}`))
- })
+ this.db.gridfs
+ .removeAsync({ _id: assetId })
+ .then(() => {
+ res.json({})
+ })
+ .catch((err) => {
+ next(
+ createError.BadRequest(
+ `Unable to delete asset '${assetId}'. ${err.message}`
+ )
+ )
+ })
}
beginAssetUpload(req, res, next) {
@@ -88,112 +118,148 @@ export class AssetRoutes {
let { fileName, fileSize, numberOfChunks, contentType } = req.body
if (!fileName || !fileSize || !numberOfChunks || !contentType) {
- return next(createError.BadRequest('Must specify fileName, fileSize, numberOfChunks and Content-Type header'))
+ return next(
+ createError.BadRequest(
+ "Must specify fileName, fileSize, numberOfChunks and Content-Type header"
+ )
+ )
}
- fileName = uploadId + '-' + path.basename(fileName)
+ fileName = uploadId + "-" + path.basename(fileName)
- this.rs.setAsync(
- uploadId, JSON.stringify({
- fileName, fileSize, numberOfChunks, contentType
- }), 'EX', this.uploadTimeout).then(() => {
- res.json({ uploadId })
- }).catch((error) => {
- next(createError.InternalServerError(error.message))
- })
+ this.rs
+ .setAsync(
+ uploadId,
+ JSON.stringify({
+ fileName,
+ fileSize,
+ numberOfChunks,
+ contentType,
+ }),
+ "EX",
+ this.uploadTimeout
+ )
+ .then(() => {
+ res.json({ uploadId })
+ })
+ .catch((error) => {
+ next(createError.InternalServerError(error.message))
+ })
}
continueAssetUpload(req, res, next) {
if (!(req.body instanceof Buffer)) {
- return next(createError.BadRequest('Body must be of type application/octet-stream'))
+ return next(
+ createError.BadRequest("Body must be of type application/octet-stream")
+ )
}
- const range = req.get('Range')
- const contentLength = req.get('Content-Length')
+ const range = req.get("Range")
+ const contentLength = req.get("Content-Length")
let match = range.match(AssetRoutes.rangeRegex)
let offset = null
if (!match || match.length < 2 || (offset = parseInt(match[1])) === NaN) {
- return next(createError.BadRequest('Range header must be supplied and of form \'byte \''))
+ return next(
+ createError.BadRequest(
+ "Range header must be supplied and of form 'byte '"
+ )
+ )
}
if (parseInt(contentLength, 10) !== req.body.length) {
- return next(createError.BadRequest('Must supply Content-Length header matching length of request body'))
+ return next(
+ createError.BadRequest(
+ "Must supply Content-Length header matching length of request body"
+ )
+ )
}
const uploadId = req.params._id
- const uploadCountId = uploadId + '$#'
- const uploadDataId = uploadId + '$@'
+ const uploadCountId = uploadId + "$#"
+ const uploadDataId = uploadId + "$@"
- this.rs.getAsync(uploadId).then((content) => {
- let uploadData = null
+ this.rs
+ .getAsync(uploadId)
+ .then((content) => {
+ let uploadData = null
- try {
- uploadData = JSON.parse(content)
- } catch (error){
- return Promise.reject(new Error('Could not parse upload data'))
- }
-
- if (offset < 0 || offset + req.body.length > uploadData.fileSize) {
- return Promise.reject(new Error(`Illegal range offset ${offset} given`))
- }
-
- Promise.all([
- this.rs.setrangeAsync(uploadDataId, offset, req.body),
- this.rs.incrAsync(uploadCountId)
- ]).then((arr) => {
- const uploadedChunks = arr[1]
- let chunkInfo = {
- numberOfChunks: uploadData.numberOfChunks,
- uploadedChunks
+ try {
+ uploadData = JSON.parse(content)
+ } catch (error) {
+ return Promise.reject(new Error("Could not parse upload data"))
}
- if (uploadedChunks >= uploadData.numberOfChunks) {
- let readable = redisReadStream(this.rs.client, Buffer(uploadDataId))
- let writeable = this.db.gridfs.createWriteStream({
- _id: uploadId,
- filename: uploadData.fileName,
- content_type: uploadData.contentType
- })
-
- let promise = pipeToGridFS(readable, writeable).then((file) => {
- return Promise.all([
- Promise.resolve(file),
- this.rs.del(uploadId),
- this.rs.del(uploadCountId),
- this.rs.del(uploadDataId)
- ])
- }).then((arr) => {
- const [file] = arr
- res.json({
- assetId: file._id,
- fileName: file.filename,
- contentType: file.contentType,
- uploadDate: file.uploadDate,
- md5: file.md5,
- ...chunkInfo
- })
- }) // TODO: Test that this will be caught...
- return promise
- } else {
- return Promise.all([
- this.rs.expireAsync(uploadId, this.uploadTimeout),
- this.rs.expireAsync(uploadCountId, this.uploadTimeout),
- this.rs.expireAsync(uploadDataId, this.uploadTimeout)
- ]).then(() => {
- res.json(chunkInfo)
- })
+ if (offset < 0 || offset + req.body.length > uploadData.fileSize) {
+ return Promise.reject(
+ new Error(`Illegal range offset ${offset} given`)
+ )
}
- }).catch((error) => {
- this.rs.del(uploadId)
- this.rs.del(uploadCountId)
- this.rs.del(uploadDataId)
- console.error(error) // TODO: This should go into log file
- next(createError.BadRequest('Unable to upload data chunk'))
+
+ Promise.all([
+ this.rs.setrangeAsync(uploadDataId, offset, req.body),
+ this.rs.incrAsync(uploadCountId),
+ ])
+ .then((arr) => {
+ const uploadedChunks = arr[1]
+ let chunkInfo = {
+ numberOfChunks: uploadData.numberOfChunks,
+ uploadedChunks,
+ }
+
+ if (uploadedChunks >= uploadData.numberOfChunks) {
+ let readable = redisReadStream(
+ this.rs.client,
+ Buffer(uploadDataId)
+ )
+ let writeable = this.db.gridfs.createWriteStream({
+ _id: uploadId,
+ filename: uploadData.fileName,
+ content_type: uploadData.contentType,
+ })
+
+ let promise = pipeToGridFS(readable, writeable)
+ .then((file) => {
+ return Promise.all([
+ Promise.resolve(file),
+ this.rs.del(uploadId),
+ this.rs.del(uploadCountId),
+ this.rs.del(uploadDataId),
+ ])
+ })
+ .then((arr) => {
+ const [file] = arr
+ res.json({
+ assetId: file._id,
+ fileName: file.filename,
+ contentType: file.contentType,
+ uploadDate: file.uploadDate,
+ md5: file.md5,
+ ...chunkInfo,
+ })
+ }) // TODO: Test that this will be caught...
+ return promise
+ } else {
+ return Promise.all([
+ this.rs.expireAsync(uploadId, this.uploadTimeout),
+ this.rs.expireAsync(uploadCountId, this.uploadTimeout),
+ this.rs.expireAsync(uploadDataId, this.uploadTimeout),
+ ]).then(() => {
+ res.json(chunkInfo)
+ })
+ }
+ })
+ .catch((error) => {
+ this.rs.del(uploadId)
+ this.rs.del(uploadCountId)
+ this.rs.del(uploadDataId)
+ console.error(error) // TODO: This should go into log file
+ next(createError.BadRequest("Unable to upload data chunk"))
+ })
+ })
+ .catch((error) => {
+ console.error(error) // TODO: This should go into log file
+ next(createError.BadRequest(error.message))
})
- }).catch((error) => {
- console.error(error) // TODO: This should go into log file
- next(createError.BadRequest(error.message))
- })
}
}
diff --git a/server/src/api/routes/AuthRoutes.js b/server/src/api/routes/AuthRoutes.js
index 7d06e68..3365347 100644
--- a/server/src/api/routes/AuthRoutes.js
+++ b/server/src/api/routes/AuthRoutes.js
@@ -1,13 +1,14 @@
-import passport from 'passport'
-import credential from 'credential'
-import createError from 'http-errors'
-import config from 'config'
-import crypto from 'crypto'
-import urlSafeBase64 from 'urlsafe-base64'
-import util from 'util'
-import * as loginToken from './loginToken'
-import autobind from 'autobind-decorator'
-import url from 'url'
+import passport from "passport"
+import credential from "credential"
+import createError from "http-errors"
+import config from "config"
+import crypto from "crypto"
+import urlSafeBase64 from "urlsafe-base64"
+import util from "util"
+import * as loginToken from "./loginToken"
+import autobind from "autobind-decorator"
+import url from "url"
+import { catchAll } from "."
@autobind
export class AuthRoutes {
@@ -16,44 +17,60 @@ export class AuthRoutes {
this.mq = container.mq
this.db = container.db
- this.maxEmailTokenAgeInHours = config.get('email.maxEmailTokenAgeInHours')
- this.maxPasswordTokenAgeInHours = config.get('email.maxPasswordTokenAgeInHours')
- this.sendEmailDelayInSeconds = config.get('email.sendEmailDelayInSeconds')
- this.supportEmail = config.get('email.supportEmail')
- this.sendEmail = config.get('email.sendEmail')
- app.route('/auth/login')
+ this.maxEmailTokenAgeInHours = config.get("email.maxEmailTokenAgeInHours")
+ this.maxPasswordTokenAgeInHours = config.get(
+ "email.maxPasswordTokenAgeInHours"
+ )
+ this.sendEmailDelayInSeconds = config.get("email.sendEmailDelayInSeconds")
+ this.supportEmail = config.get("email.supportEmail")
+ this.sendEmail = config.get("email.sendEmail")
+ app
+ .route("/auth/login")
// Used to login. Email must be confirmed.
- .post(this.login)
+ .post(catchAll(this.login))
// Used to logout
- .delete(passport.authenticate('bearer', { session: false }), this.logout)
+ .delete(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.logout)
+ )
// Send change email confirmation email
- app.route('/auth/email/send')
- .post(passport.authenticate('bearer', { session: false }), this.sendChangeEmailEmail)
+ app
+ .route("/auth/email/send")
+ .post(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.sendChangeEmailEmail)
+ )
// Confirm email address
- app.route('/auth/email/confirm')
- .post(this.confirmEmail)
+ app.route("/auth/email/confirm").post(catchAll(this.confirmEmail))
// Change the logged in users password, leaving user still logged in
- app.route('/auth/password/change')
- .post(passport.authenticate('bearer', { session: false }), this.changePassword)
+ app
+ .route("/auth/password/change")
+ .post(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.changePassword)
+ )
// Send a password reset email
- app.route('/auth/password/send')
- .post(this.sendPasswordResetEmail)
+ app.route("/auth/password/send").post(catchAll(this.sendPasswordResetEmail))
// Confirm a password reset token is valid
- app.route('/auth/password/confirm')
- .post(this.confirmPasswordToken)
+ app
+ .route("/auth/password/confirm")
+ .post(catchAll(this.confirmPasswordToken))
// Finish a password reset
- app.route('/auth/password/reset')
- .post(this.resetPassword)
+ app.route("/auth/password/reset").post(catchAll(this.resetPassword))
// Indicate who the currently logged in user is
- app.route('/auth/who')
- .get(passport.authenticate('bearer', { session: false }), this.whoAmI)
+ app
+ .route("/auth/who")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.whoAmI)
+ )
}
async login(req, res, next) {
@@ -61,62 +78,53 @@ export class AuthRoutes {
const password = req.body.password
let User = this.db.User
- try {
- if (!email || !password) {
- createError.BadRequest('Must supply user name and password')
- }
+ if (!email || !password) {
+ createError.BadRequest("Must supply user name and password")
+ }
- // Lookup the user
- const user = await User.findOne({ email })
+ // Lookup the user
+ const user = await User.findOne({ email })
- if (!user) {
- // NOTE: Don't return NotFound as that gives too much information away to hackers
- throw createError.BadRequest("Email or password incorrect")
- } else if (user.emailToken || !user.passwordHash) {
- throw createError.Forbidden("Must confirm email and set password")
- }
+ if (!user) {
+ // NOTE: Don't return NotFound as that gives too much information away to hackers
+ throw createError.BadRequest("Email or password incorrect")
+ } else if (user.emailToken || !user.passwordHash) {
+ throw createError.Forbidden("Must confirm email and set password")
+ }
- let cr = credential()
- const isValid = await cr.verify(JSON.stringify(user.passwordHash), req.body.password)
+ let cr = credential()
+ const isValid = await cr.verify(
+ JSON.stringify(user.passwordHash),
+ req.body.password
+ )
- if (isValid) {
- user.loginToken = loginToken.pack(user._id.toString(), user.email)
- } else {
- user.loginToken = null // A bad login removes existing token for this user...
- }
+ if (isValid) {
+ user.loginToken = loginToken.pack(user._id.toString(), user.email)
+ } else {
+ user.loginToken = null // A bad login removes existing token for this user...
+ }
- const savedUser = await user.save()
+ const savedUser = await user.save()
- if (savedUser.loginToken) {
- res.set('Authorization', `Bearer ${savedUser.loginToken}`)
- res.json(savedUser.toClient())
- } else {
- throw createError.BadRequest('email or password incorrect')
- }
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (savedUser.loginToken) {
+ res.set("Authorization", `Bearer ${savedUser.loginToken}`)
+ res.json(savedUser.toClient())
+ } else {
+ throw createError.BadRequest("email or password incorrect")
}
}
async logout(req, res, next) {
let User = this.db.User
- try {
- const user = await User.findById({ _id: req.user._id })
+ const user = await User.findById({ _id: req.user._id })
- if (user) {
- user.loginToken = null
- await user.save()
- }
-
- res.json({})
- } catch(err) {
- next(createError.InternalServerError(err.message))
+ if (user) {
+ user.loginToken = null
+ await user.save()
}
+
+ res.json({})
}
whoAmI(req, res, next) {
@@ -129,175 +137,163 @@ export class AuthRoutes {
let User = this.db.User
const isAdmin = !!req.user.administrator
- try {
- if (existingEmail) {
- if (!isAdmin) {
- throw createError.Forbidden('Only admins can resend change email to any user')
- }
- } else {
- existingEmail = req.user.email
+ if (existingEmail) {
+ if (!isAdmin) {
+ throw createError.Forbidden(
+ "Only admins can resend change email to any user"
+ )
}
+ } else {
+ existingEmail = req.user.email
+ }
- const user = await User.findOne({ email: existingEmail })
- let conflictingUser = null
+ const user = await User.findOne({ email: existingEmail })
+ let conflictingUser = null
- if (newEmail) {
- conflictingUser = await User.findOne({ email: newEmail })
- }
+ if (newEmail) {
+ conflictingUser = await User.findOne({ email: newEmail })
+ }
- if (!user) {
- throw createError.NotFound(`User with email '${existingEmail}' was not found`)
- } else if (conflictingUser) {
- throw createError.BadRequest(`A user with '${newEmail}' already exists`)
- } else if (!isAdmin && user.emailToken && (new Date() - user.emailToken.created) < this.sendEmailDelayInSeconds) {
- throw createError.BadRequest('Cannot request email confirmation again so soon')
- }
+ if (!user) {
+ throw createError.NotFound(
+ `User with email '${existingEmail}' was not found`
+ )
+ } else if (conflictingUser) {
+ throw createError.BadRequest(`A user with '${newEmail}' already exists`)
+ } else if (
+ !isAdmin &&
+ user.emailToken &&
+ new Date() - user.emailToken.created < this.sendEmailDelayInSeconds
+ ) {
+ throw createError.BadRequest(
+ "Cannot request email confirmation again so soon"
+ )
+ }
- const buf = await util.promisify(crypto.randomBytes)(32)
+ const buf = await util.promisify(crypto.randomBytes)(32)
- user.emailToken = {
- value: urlSafeBase64.encode(buf),
- created: new Date()
- }
+ user.emailToken = {
+ value: urlSafeBase64.encode(buf),
+ created: new Date(),
+ }
- if (newEmail) {
- user.newEmail = newEmail
- }
+ if (newEmail) {
+ user.newEmail = newEmail
+ }
- const savedUser = await user.save()
- const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
- const siteUrl = url.parse(req.headers.referer)
- let msgs = []
-
- if (savedUser.newEmail) {
- msgs.push({
- toEmail: savedUser.email,
- templateName: 'changeEmailOld',
- templateData: {
- recipientFullName: userFullName,
- recipientNewEmail: savedUser.newEmail,
- supportEmail: this.supportEmail
- }
- })
- }
+ const savedUser = await user.save()
+ const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
+ const siteUrl = url.parse(req.headers.referer)
+ let msgs = []
+ if (savedUser.newEmail) {
msgs.push({
- toEmail: savedUser.newEmail || savedUser.email,
- templateName: 'changeEmailNew',
+ toEmail: savedUser.email,
+ templateName: "changeEmailOld",
templateData: {
recipientFullName: userFullName,
- confirmEmailLink: `${siteUrl.protocol}//${siteUrl.host}/confirm-email?email-token%3D${savedUser.emailToken.value}`,
- supportEmail: this.supportEmail
- }
+ recipientNewEmail: savedUser.newEmail,
+ supportEmail: this.supportEmail,
+ },
})
-
- if (this.sendEmail) {
- await this.mq.request('dar-email', 'sendEmail', msgs)
- }
-
- res.json({})
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
}
+
+ msgs.push({
+ toEmail: savedUser.newEmail || savedUser.email,
+ templateName: "changeEmailNew",
+ templateData: {
+ recipientFullName: userFullName,
+ confirmEmailLink: `${siteUrl.protocol}//${
+ siteUrl.host
+ }/confirm-email?email-token%3D${savedUser.emailToken.value}`,
+ supportEmail: this.supportEmail,
+ },
+ })
+
+ if (this.sendEmail) {
+ await this.mq.request("dar-email", "sendEmail", msgs)
+ }
+
+ res.json({})
}
async confirmEmail(req, res, next) {
const token = req.body.emailToken
let User = this.db.User
- try {
- if (!token) {
- throw createError.BadRequest('Invalid request parameters')
- }
+ if (!token) {
+ throw createError.BadRequest("Invalid request parameters")
+ }
- const user = await User.findOne({ 'emailToken.value': token })
+ const user = await User.findOne({ "emailToken.value": token })
- if (!user) {
- throw createError.BadRequest(`The token was not found`)
- }
+ if (!user) {
+ throw createError.BadRequest(`The token was not found`)
+ }
- // Token must not be too old
- const ageInHours = (new Date() - user.emailToken.created) / 3600000
+ // Token must not be too old
+ const ageInHours = (new Date() - user.emailToken.created) / 3600000
- if (ageInHours > this.maxEmailTokenAgeInHours) {
- throw createError.BadRequest(`Token has expired`)
- }
+ if (ageInHours > this.maxEmailTokenAgeInHours) {
+ throw createError.BadRequest(`Token has expired`)
+ }
- // Remove the email token & any login token as it will become invalid
- user.emailToken = undefined
- user.loginToken = undefined
+ // Remove the email token & any login token as it will become invalid
+ user.emailToken = undefined
+ user.loginToken = undefined
- // Switch in any new email now
- if (user.newEmail) {
- user.email = user.newEmail
- user.newEmail = undefined
- }
+ // Switch in any new email now
+ if (user.newEmail) {
+ user.email = user.newEmail
+ user.newEmail = undefined
+ }
- let buf = null
+ let buf = null
- // If user has no password, create reset token for them
- if (!user.passwordHash) {
- buf = await util.promisify(crypto.randomBytes)(32)
+ // If user has no password, create reset token for them
+ if (!user.passwordHash) {
+ buf = await util.promisify(crypto.randomBytes)(32)
- user.passwordToken = {
- value: urlSafeBase64.encode(buf),
- created: new Date()
- }
- }
-
- const savedUser = await user.save()
- let obj = {}
-
- // Only because the user has sent us a valid email reset token
- // can we respond with an password reset token.
- if (savedUser.passwordToken) {
- obj.passwordToken = savedUser.passwordToken.value
- }
-
- res.json(obj)
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
+ user.passwordToken = {
+ value: urlSafeBase64.encode(buf),
+ created: new Date(),
}
}
+
+ const savedUser = await user.save()
+ let obj = {}
+
+ // Only because the user has sent us a valid email reset token
+ // can we respond with an password reset token.
+ if (savedUser.passwordToken) {
+ obj.passwordToken = savedUser.passwordToken.value
+ }
+
+ res.json(obj)
}
async confirmPasswordToken(req, res, next) {
const token = req.body.passwordToken
let User = this.db.User
- try {
- if (!token) {
- throw createError.BadRequest('Invalid request parameters')
- }
-
- const user = await User.findOne({ 'passwordToken.value': token })
-
- if (!user) {
- throw createError.BadRequest(`The token was not found`)
- }
-
- // Token must not be too old
- const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
-
- if (ageInHours > this.maxPasswordTokenAgeInHours) {
- throw createError.BadRequest(`Token has expired`)
- }
-
- res.json({})
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!token) {
+ throw createError.BadRequest("Invalid request parameters")
}
+
+ const user = await User.findOne({ "passwordToken.value": token })
+
+ if (!user) {
+ throw createError.BadRequest(`The token was not found`)
+ }
+
+ // Token must not be too old
+ const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
+
+ if (ageInHours > this.maxPasswordTokenAgeInHours) {
+ throw createError.BadRequest(`Token has expired`)
+ }
+
+ res.json({})
}
async resetPassword(req, res, next) {
@@ -306,118 +302,102 @@ export class AuthRoutes {
let User = this.db.User
let cr = credential()
- try {
- if (!token || !newPassword) {
- throw createError.BadRequest('Invalid request parameters')
- }
-
- const user = await User.findOne({ 'passwordToken.value': token })
-
- if (!user) {
- throw createError.BadRequest(`The token was not found`)
- }
-
- // Token must not be too old
- const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
-
- if (ageInHours > this.maxPasswordTokenAgeInHours) {
- throw createError.BadRequest(`Token has expired`)
- }
-
- // Remove the password token & any login token
- user.passwordToken = undefined
- user.loginToken = undefined
-
- const json = await cr.hash(newPassword)
-
- user.passwordHash = JSON.parse(json)
- await user.save()
- res.json({})
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!token || !newPassword) {
+ throw createError.BadRequest("Invalid request parameters")
}
+
+ const user = await User.findOne({ "passwordToken.value": token })
+
+ if (!user) {
+ throw createError.BadRequest(`The token was not found`)
+ }
+
+ // Token must not be too old
+ const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
+
+ if (ageInHours > this.maxPasswordTokenAgeInHours) {
+ throw createError.BadRequest(`Token has expired`)
+ }
+
+ // Remove the password token & any login token
+ user.passwordToken = undefined
+ user.loginToken = undefined
+
+ const json = await cr.hash(newPassword)
+
+ user.passwordHash = JSON.parse(json)
+ await user.save()
+ res.json({})
}
async changePassword(req, res, next) {
let User = this.db.User
let cr = credential()
- try {
- const user = await User.findById({ _id: req.user._id })
+ const user = await User.findById({ _id: req.user._id })
- if (!user) {
- throw createError.NotFound(`User ${req.user._id} not found`)
- }
-
- const ok = await cr.verify(JSON.stringify(user.passwordHash), req.body.oldPassword)
- const obj = await cr.hash(req.body.newPassword)
-
- user.passwordHash = JSON.parse(obj)
- await user.save()
- res.json({})
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!user) {
+ throw createError.NotFound(`User ${req.user._id} not found`)
}
+
+ const ok = await cr.verify(
+ JSON.stringify(user.passwordHash),
+ req.body.oldPassword
+ )
+ const obj = await cr.hash(req.body.newPassword)
+
+ user.passwordHash = JSON.parse(obj)
+ await user.save()
+ res.json({})
}
- async sendPasswordResetEmail(req, res, next){
+ async sendPasswordResetEmail(req, res, next) {
const email = req.body.email
let User = this.db.User
- try {
- if (!email) {
- throw createError.BadRequest('Invalid request parameters')
- }
-
- const user = await User.findOne({ email })
-
- // User must exist and their email must be confirmed
- if (!user || user.emailToken) {
- // Don't give away any information about why we rejected the request
- throw createError.BadRequest('Not a valid request')
- } else if (user.passwordToken && user.passwordToken.created &&
- (new Date() - user.passwordToken.created) < this.sendEmailDelayInSeconds) {
- throw createError.BadRequest('Cannot request password reset so soon')
- }
-
- const buf = await util.promisify(crypto.randomBytes)(32)
-
- user.passwordToken = {
- value: urlSafeBase64.encode(buf),
- created: new Date()
- }
-
- const savedUser = await user.save()
- const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
- const siteUrl = url.parse(req.headers.referer)
- const msg = {
- toEmail: savedUser.email,
- templateName: 'forgotPassword',
- templateData: {
- recipientFullName: userFullName,
- resetPasswordLink: `${siteUrl.protocol}//${siteUrl.host}/reset-password?password-token%3D${savedUser.passwordToken.value}`,
- supportEmail: this.supportEmail
- }
- }
- if (this.sendEmail) {
- await this.mq.request('dar-email', 'sendEmail', msg)
- }
-
- res.json({})
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(`Unable to send password reset email. ${err.message}`))
- }
+ if (!email) {
+ throw createError.BadRequest("Invalid request parameters")
}
+
+ const user = await User.findOne({ email })
+
+ // User must exist and their email must be confirmed
+ if (!user || user.emailToken) {
+ // Don't give away any information about why we rejected the request
+ throw createError.BadRequest("Not a valid request")
+ } else if (
+ user.passwordToken &&
+ user.passwordToken.created &&
+ new Date() - user.passwordToken.created < this.sendEmailDelayInSeconds
+ ) {
+ throw createError.BadRequest("Cannot request password reset so soon")
+ }
+
+ const buf = await util.promisify(crypto.randomBytes)(32)
+
+ user.passwordToken = {
+ value: urlSafeBase64.encode(buf),
+ created: new Date(),
+ }
+
+ const savedUser = await user.save()
+ const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
+ const siteUrl = url.parse(req.headers.referer)
+ const msg = {
+ toEmail: savedUser.email,
+ templateName: "forgotPassword",
+ templateData: {
+ recipientFullName: userFullName,
+ resetPasswordLink: `${siteUrl.protocol}//${
+ siteUrl.host
+ }/reset-password?password-token%3D${savedUser.passwordToken.value}`,
+ supportEmail: this.supportEmail,
+ },
+ }
+ if (this.sendEmail) {
+ await this.mq.request("dar-email", "sendEmail", msg)
+ }
+
+ res.json({})
}
}
diff --git a/server/src/api/routes/TeamRoutes.js b/server/src/api/routes/TeamRoutes.js
index 6c487be..cb651c0 100644
--- a/server/src/api/routes/TeamRoutes.js
+++ b/server/src/api/routes/TeamRoutes.js
@@ -1,6 +1,7 @@
-import passport from 'passport'
-import createError from 'http-errors'
-import autobind from 'autobind-decorator'
+import passport from "passport"
+import createError from "http-errors"
+import autobind from "autobind-decorator"
+import { catchAll } from "."
@autobind
export class TeamRoutes {
@@ -12,14 +13,31 @@ export class TeamRoutes {
this.mq = container.mq
this.ws = container.ws
- app.route('/teams')
- .get(passport.authenticate('bearer', { session: false }), this.listTeams)
- .post(passport.authenticate('bearer', { session: false }), this.createTeam)
- .put(passport.authenticate('bearer', { session: false }), this.updateTeam)
+ app
+ .route("/teams")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.listTeams)
+ )
+ .post(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.createTeam)
+ )
+ .put(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.updateTeam)
+ )
- app.route('/teams/:_id([a-f0-9]{24})')
- .get(passport.authenticate('bearer', { session: false }), this.getTeam)
- .delete(passport.authenticate('bearer', { session: false }), this.deleteTeam)
+ app
+ .route("/teams/:_id([a-f0-9]{24})")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.getTeam)
+ )
+ .delete(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.deleteTeam)
+ )
}
async listTeams(req, res, next) {
@@ -29,139 +47,103 @@ export class TeamRoutes {
let partial = !!req.query.partial
let query = {}
- try {
- const total = await Team.count({})
- let teams = []
- let cursor = Team.find(query).limit(limit).skip(skip).cursor().map((doc) => {
+ const total = await Team.count({})
+ let teams = []
+ let cursor = Team.find(query)
+ .limit(limit)
+ .skip(skip)
+ .cursor()
+ .map((doc) => {
return doc.toClient(partial)
})
- cursor.on('data', (doc) => {
- teams.push(doc)
+ cursor.on("data", (doc) => {
+ teams.push(doc)
+ })
+ cursor.on("end", () => {
+ res.json({
+ total: total,
+ offset: skip,
+ count: teams.length,
+ items: teams,
})
- cursor.on('end', () => {
- res.json({
- total: total,
- offset: skip,
- count: teams.length,
- items: teams
- })
- })
- cursor.on('error', (err) => {
- throw err
- })
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
- }
+ })
+ cursor.on("error", (err) => {
+ throw err
+ })
}
async createTeam(req, res, next) {
- try {
- if (!req.user.administrator) {
- throw createError.Forbidden()
- }
-
- // Create a new Team template then assign it to a value in the req.body
- const Team = this.db.Team
- let team = new Team(req.body)
-
- const newTeam = await team.save()
-
- res.json(newTeam.toClient())
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!req.user.administrator) {
+ throw createError.Forbidden()
}
+
+ // Create a new Team template then assign it to a value in the req.body
+ const Team = this.db.Team
+ let team = new Team(req.body)
+
+ const newTeam = await team.save()
+
+ res.json(newTeam.toClient())
}
async updateTeam(req, res, next) {
- try {
- if (!req.user.administrator) {
- throw createError.Forbidden()
- }
-
- // Do this here because Mongoose will add it automatically otherwise
- if (!req.body._id) {
- throw createError.BadRequest('No _id given in body')
- }
-
- let Team = this.db.Team
- let teamUpdates = null
-
- try {
- teamUpdates = new Team(req.body)
- } catch (err) {
- throw createError.BadRequest('Invalid data')
- }
-
- const foundTeam = await Team.findById(teamUpdates._id)
-
- if (!foundTeam) {
- throw createError.NotFound(`Team with _id ${_id} was not found`)
- }
- foundTeam.merge(teamUpdates)
- const savedTeam = await foundTeam.save()
-
- res.json(savedTeam.toClient())
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!req.user.administrator) {
+ throw createError.Forbidden()
}
+
+ // Do this here because Mongoose will add it automatically otherwise
+ if (!req.body._id) {
+ throw createError.BadRequest("No _id given in body")
+ }
+
+ let Team = this.db.Team
+ let teamUpdates = null
+
+ try {
+ teamUpdates = new Team(req.body)
+ } catch (err) {
+ throw createError.BadRequest("Invalid data")
+ }
+
+ const foundTeam = await Team.findById(teamUpdates._id)
+
+ if (!foundTeam) {
+ throw createError.NotFound(`Team with _id ${_id} was not found`)
+ }
+ foundTeam.merge(teamUpdates)
+ const savedTeam = await foundTeam.save()
+
+ res.json(savedTeam.toClient())
}
async getTeam(req, res, next) {
const Team = this.db.Team
const _id = req.params._id
- try {
- const team = await Team.findById(_id)
+ const team = await Team.findById(_id)
- if (!team) {
- throw createError.NotFound(`Team with _id ${_id} not found`)
- }
-
- res.json(team.toClient())
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!team) {
+ throw createError.NotFound(`Team with _id ${_id} not found`)
}
+
+ res.json(team.toClient())
}
async deleteTeam(req, res, next) {
- try {
- if (!req.user.administrator) {
- throw createError.Forbidden()
- }
-
- const Team = this.db.Team
- const _id = req.params._id
-
- const removedTeam = await Team.remove({ _id })
-
- if (!removedTeam) {
- throw createError.NotFound(`Team with _id ${_id} not found`)
- }
-
- res.json({})
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!req.user.administrator) {
+ throw createError.Forbidden()
}
+
+ const Team = this.db.Team
+ const _id = req.params._id
+
+ const removedTeam = await Team.remove({ _id })
+
+ if (!removedTeam) {
+ throw createError.NotFound(`Team with _id ${_id} not found`)
+ }
+
+ res.json({})
}
}
diff --git a/server/src/api/routes/UserRoutes.js b/server/src/api/routes/UserRoutes.js
index f7eb204..3c840da 100644
--- a/server/src/api/routes/UserRoutes.js
+++ b/server/src/api/routes/UserRoutes.js
@@ -1,11 +1,12 @@
-import passport from 'passport'
-import createError from 'http-errors'
-import crypto from 'crypto'
-import urlSafeBase64 from 'urlsafe-base64'
-import url from 'url'
-import util from 'util'
-import autobind from 'autobind-decorator'
-import config from 'config'
+import passport from "passport"
+import createError from "http-errors"
+import crypto from "crypto"
+import urlSafeBase64 from "urlsafe-base64"
+import url from "url"
+import util from "util"
+import autobind from "autobind-decorator"
+import config from "config"
+import { catchAll } from "."
@autobind
export class UserRoutes {
@@ -16,23 +17,48 @@ export class UserRoutes {
this.db = container.db
this.mq = container.mq
this.ws = container.ws
- this.maxEmailTokenAgeInHours = config.get('email.maxEmailTokenAgeInHours')
- this.sendEmail = config.get('email.sendEmail')
- app.route('/users')
- .get(passport.authenticate('bearer', { session: false }), this.listUsers)
+ this.maxEmailTokenAgeInHours = config.get("email.maxEmailTokenAgeInHours")
+ this.sendEmail = config.get("email.sendEmail")
+ app
+ .route("/users")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.listUsers)
+ )
// Add a new user, send email confirmation email
- .post(passport.authenticate('bearer', { session: false }), this.createUser)
- .put(passport.authenticate('bearer', { session: false }), this.updateUser)
+ .post(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.createUser)
+ )
+ .put(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.updateUser)
+ )
- app.route('/users/:_id([a-f0-9]{24})')
- .get(passport.authenticate('bearer', { session: false }), this.getUser)
- .delete(passport.authenticate('bearer', { session: false }), this.deleteUser)
+ app
+ .route("/users/:_id([a-f0-9]{24})")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.getUser)
+ )
+ .delete(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.deleteUser)
+ )
- app.route('/users/enter-room/:roomName')
- .put(passport.authenticate('bearer', { session: false }), this.enterRoom)
+ app
+ .route("/users/enter-room/:roomName")
+ .put(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.enterRoom)
+ )
- app.route('/users/leave-room')
- .put(passport.authenticate('bearer', { session: false }), this.leaveRoom)
+ app
+ .route("/users/leave-room")
+ .put(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.leaveRoom)
+ )
}
async listUsers(req, res, next) {
@@ -44,169 +70,145 @@ export class UserRoutes {
const team = req.query.team
let query = {}
- try {
- if (team) {
- query = { team }
- }
+ if (team) {
+ query = { team }
+ }
- if (!isAdmin) {
- throw createError.Forbidden()
- }
+ if (!isAdmin) {
+ throw createError.Forbidden()
+ }
- const total = await User.count({})
- let users = []
- let cursor = User.find(query).limit(limit).skip(skip).cursor().map((doc) => {
+ const total = await User.count({})
+ let users = []
+ let cursor = User.find(query)
+ .limit(limit)
+ .skip(skip)
+ .cursor()
+ .map((doc) => {
return doc.toClient(req.user)
})
- cursor.on('data', (doc) => {
- users.push(doc)
+ cursor.on("data", (doc) => {
+ users.push(doc)
+ })
+ cursor.on("end", () => {
+ res.json({
+ total: total,
+ offset: skip,
+ count: users.length,
+ items: users,
})
- cursor.on('end', () => {
- res.json({
- total: total,
- offset: skip,
- count: users.length,
- items: users
- })
- })
- cursor.on('error', (err) => {
- throw err
- })
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
- }
+ })
+ cursor.on("error", (err) => {
+ throw err
+ })
}
async getUser(req, res, next) {
let User = this.db.User
const _id = req.params._id
- const isSelf = (_id === req.user._id)
+ const isSelf = _id === req.user._id
const isAdmin = req.user.administrator
- try {
- // User can see themselves, otherwise must be super user
- if (!isSelf && !isAdmin) {
- throw createError.Forbidden()
- }
-
- const user = await User.findById(_id)
-
- if (!user) {
- return Promise.reject(createError.NotFound(`User with _id ${_id} was not found`))
- }
-
- res.json(user.toClient(req.user))
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ // User can see themselves, otherwise must be super user
+ if (!isSelf && !isAdmin) {
+ throw createError.Forbidden()
}
+
+ const user = await User.findById(_id)
+
+ if (!user) {
+ return Promise.reject(
+ createError.NotFound(`User with _id ${_id} was not found`)
+ )
+ }
+
+ res.json(user.toClient(req.user))
}
async createUser(req, res, next) {
const isAdmin = req.user.administrator
- try {
- if (!isAdmin) {
- throw new createError.Forbidden()
- }
+ if (!isAdmin) {
+ throw new createError.Forbidden()
+ }
- let User = this.db.User
- let user = new User(req.body)
+ let User = this.db.User
+ let user = new User(req.body)
- // Add email confirmation required token
- const buf = await util.promisify(crypto.randomBytes)(32)
+ // Add email confirmation required token
+ const buf = await util.promisify(crypto.randomBytes)(32)
- user.emailToken = {
- value: urlSafeBase64.encode(buf),
- created: new Date()
- }
- const savedUser = await user.save()
- const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
- const senderFullName = `${req.user.firstName} ${req.user.lastName}`
- const siteUrl = url.parse(req.headers.referer)
- const msg = {
- toEmail: savedUser.email,
- templateName: 'welcome',
- templateData: {
- recipientFullName: userFullName,
- senderFullName: senderFullName,
- confirmEmailLink: `${siteUrl.protocol}//${siteUrl.host}/confirm-email?email-token%3D${savedUser.emailToken.value}`,
- confirmEmailLinkExpirationHours: this.maxEmailTokenAgeInHours,
- supportEmail: this.supportEmail
- }
- }
+ user.emailToken = {
+ value: urlSafeBase64.encode(buf),
+ created: new Date(),
+ }
+ const savedUser = await user.save()
+ const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
+ const senderFullName = `${req.user.firstName} ${req.user.lastName}`
+ const siteUrl = url.parse(req.headers.referer)
+ const msg = {
+ toEmail: savedUser.email,
+ templateName: "welcome",
+ templateData: {
+ recipientFullName: userFullName,
+ senderFullName: senderFullName,
+ confirmEmailLink: `${siteUrl.protocol}//${
+ siteUrl.host
+ }/confirm-email?email-token%3D${savedUser.emailToken.value}`,
+ confirmEmailLinkExpirationHours: this.maxEmailTokenAgeInHours,
+ supportEmail: this.supportEmail,
+ },
+ }
- res.json(savedUser.toClient())
+ res.json(savedUser.toClient())
- if (this.sendEmail) {
- await this.mq.request('dar-email', 'sendEmail', msg)
- }
- } catch(err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (this.sendEmail) {
+ await this.mq.request("dar-email", "sendEmail", msg)
}
}
async updateUser(req, res, next) {
const isAdmin = req.user.administrator
- try {
- // Do this here because Mongoose will add it automatically otherwise
- if (!req.body._id) {
- throw createError.BadRequest('No user _id given in body')
- }
-
- const isSelf = (req.body._id === req.user._id.toString())
-
- // User can change themselves, otherwise must be super user
- if (!isSelf && !isAdmin) {
- throw createError.Forbidden()
- }
-
- const User = this.db.User
- let userUpdates = null
-
- try {
- userUpdates = new User(req.body)
- } catch (err) {
- throw createError.BadRequest('Invalid data')
- }
-
- if (isSelf && !isAdmin) {
- throw createError.BadRequest('Cannot modify own administrator level')
- }
-
- const foundUser = await User.findById(userUpdates._id)
-
- if (!foundUser) {
- throw createError.NotFound(`User with _id ${user._id} was not found`)
- }
-
- // We don't allow direct updates to the email field so remove it if present
- delete userUpdates.email
-
- foundUser.merge(userUpdates)
- const savedUser = await foundUser.save()
-
- res.json(savedUser.toClient(req.user))
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ // Do this here because Mongoose will add it automatically otherwise
+ if (!req.body._id) {
+ throw createError.BadRequest("No user _id given in body")
}
+
+ const isSelf = req.body._id === req.user._id.toString()
+
+ // User can change themselves, otherwise must be super user
+ if (!isSelf && !isAdmin) {
+ throw createError.Forbidden()
+ }
+
+ const User = this.db.User
+ let userUpdates = null
+
+ try {
+ userUpdates = new User(req.body)
+ } catch (err) {
+ throw createError.BadRequest("Invalid data")
+ }
+
+ if (isSelf && !isAdmin) {
+ throw createError.BadRequest("Cannot modify own administrator level")
+ }
+
+ const foundUser = await User.findById(userUpdates._id)
+
+ if (!foundUser) {
+ throw createError.NotFound(`User with _id ${user._id} was not found`)
+ }
+
+ // We don't allow direct updates to the email field so remove it if present
+ delete userUpdates.email
+
+ foundUser.merge(userUpdates)
+ const savedUser = await foundUser.save()
+
+ res.json(savedUser.toClient(req.user))
}
enterRoom(req, res, next) {
@@ -222,41 +224,33 @@ export class UserRoutes {
async deleteUser(req, res, next) {
const isAdmin = req.user.administrator
- try {
- if (!isAdmin) {
- throw createError.Forbidden()
- }
+ if (!isAdmin) {
+ throw createError.Forbidden()
+ }
- let User = this.db.User
- const _id = req.params._id
- const deletedUser = await User.remove({ _id })
+ let User = this.db.User
+ const _id = req.params._id
+ const deletedUser = await User.remove({ _id })
- if (!deletedUser) {
- throw createError.NotFound(`User with _id ${_id} was not found`)
- }
+ if (!deletedUser) {
+ throw createError.NotFound(`User with _id ${_id} was not found`)
+ }
- const userFullName = `${deletedUser.firstName} ${deletedUser.lastName}`
- const senderFullName = `${req.user.firstName} ${req.user.lastName}`
- const msg = {
- toEmail: deletedUser.newEmail,
- templateName: 'accountDeleted',
- templateData: {
- recipientFullName: userFullName,
- senderFullName: senderFullName,
- supportEmail: this.supportEmail
- }
- }
- res.json({})
+ const userFullName = `${deletedUser.firstName} ${deletedUser.lastName}`
+ const senderFullName = `${req.user.firstName} ${req.user.lastName}`
+ const msg = {
+ toEmail: deletedUser.newEmail,
+ templateName: "accountDeleted",
+ templateData: {
+ recipientFullName: userFullName,
+ senderFullName: senderFullName,
+ supportEmail: this.supportEmail,
+ },
+ }
+ res.json({})
- if (this.sendEmail) {
- await this.mq.request('dar-email', 'sendEmail', msg)
- }
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (this.sendEmail) {
+ await this.mq.request("dar-email", "sendEmail", msg)
}
}
}
diff --git a/server/src/api/routes/WorkItemRoutes.js b/server/src/api/routes/WorkItemRoutes.js
index db6a61f..5e1e493 100644
--- a/server/src/api/routes/WorkItemRoutes.js
+++ b/server/src/api/routes/WorkItemRoutes.js
@@ -1,6 +1,7 @@
import passport from "passport"
import createError from "http-errors"
import autobind from "autobind-decorator"
+import { catchAll } from "."
@autobind
export class WorkItemRoutes {
@@ -16,183 +17,176 @@ export class WorkItemRoutes {
.route("/workitems")
.get(
passport.authenticate("bearer", { session: false }),
- this.listWorkItems
+ catchAll(this.listWorkItems)
)
.post(
passport.authenticate("bearer", { session: false }),
- this.createWorkItem
+ catchAll(this.createWorkItem)
)
.put(
passport.authenticate("bearer", { session: false }),
- this.updateWorkItem
+ catchAll(this.updateWorkItem)
+ )
+
+ app
+ .route("/workitems/activities")
+ .get(
+ passport.authenticate("bearer", { session: false }),
+ catchAll(this.listWorkItemActivities)
)
app
.route("/workitems/:_id([a-f0-9]{24})")
.get(
passport.authenticate("bearer", { session: false }),
- this.getWorkItem
+ catchAll(this.getWorkItem)
)
.delete(
passport.authenticate("bearer", { session: false }),
- this.deleteWorkItem
+ catchAll(this.deleteWorkItem)
)
}
async listWorkItems(req, res, next) {
- try {
- const WorkItem = this.db.WorkItem
- const limit = req.query.limit || 20
- const skip = req.query.skip || 0
- const partial = !!req.query.partial
- let query = {}
+ const WorkItem = this.db.WorkItem
+ const limit = req.query.limit || 20
+ const skip = req.query.skip || 0
+ const partial = !!req.query.partial
+ let query = {}
- const total = await WorkItem.count({})
+ const total = await WorkItem.count({})
- let workItems = []
- let cursor = WorkItem.find(query)
- .limit(limit)
- .skip(skip)
- .cursor()
- .map((doc) => {
- return doc.toClient(partial)
- })
+ let workItems = []
+ let cursor = WorkItem.find(query)
+ .limit(limit)
+ .skip(skip)
+ .cursor()
+ .map((doc) => {
+ return doc.toClient(partial)
+ })
- cursor.on("data", (doc) => {
- workItems.push(doc)
+ cursor.on("data", (doc) => {
+ workItems.push(doc)
+ })
+ cursor.on("end", () => {
+ res.json({
+ total: total,
+ offset: skip,
+ count: workItems.length,
+ items: workItems,
})
- cursor.on("end", () => {
- res.json({
- total: total,
- offset: skip,
- count: workItems.length,
- items: workItems,
- })
+ })
+ cursor.on("error", (err) => {
+ throw createError.InternalServerError(err.message)
+ })
+ }
+
+ async listWorkItemActivities(req, res, next) {
+ const WorkItem = this.db.WorkItem
+ const aggregate = WorkItem.aggregate()
+ .sort({ ticketNumber: 1 })
+ .lookup({
+ from: "activities",
+ localField: "_id",
+ foreignField: "workItem",
+ as: "data",
})
- cursor.on("error", (err) => {
- throw createError.InternalServerError(err.message)
+ .project({
+ workItemType: 1,
+ ticketNumber: 1,
+ address: 1,
+ "coordinate.longitude": { $arrayElemAt: ["$location.coordinates", 0] },
+ "coordinate.latitude": { $arrayElemAt: ["$location.coordinates", 1] },
+ "data._id": 1,
+ "data.resolution": 1,
+ "data.status": 1,
})
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
- }
+
+ const items = await aggregate.exec()
+
+ res.json({ items })
}
async createWorkItem(req, res, next) {
- try {
- const isAdmin = req.user.administrator
+ const isAdmin = req.user.administrator
- if (!isAdmin) {
- return new createError.Forbidden()
- }
-
- // Create a new WorkItem template then assign it to a value in the req.body
- const WorkItem = this.db.WorkItem
- let workItem = new WorkItem(req.body)
-
- // Save the workItem (with promise) - If it doesnt, catch and throw error
- const newWorkItem = await workItem.save()
-
- res.json(newWorkItem.toClient())
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!isAdmin) {
+ return new createError.Forbidden()
}
+
+ // Create a new WorkItem template then assign it to a value in the req.body
+ const WorkItem = this.db.WorkItem
+ let workItem = new WorkItem(req.body)
+
+ // Save the workItem (with promise) - If it doesnt, catch and throw error
+ const newWorkItem = await workItem.save()
+
+ res.json(newWorkItem.toClient())
}
async updateWorkItem(req, res, next) {
- try {
- const isAdmin = req.user.administrator
+ const isAdmin = req.user.administrator
- if (!isAdmin) {
- return new createError.Forbidden()
- }
-
- // Do this here because Mongoose will add it automatically otherwise
- if (!req.body._id) {
- throw createError.BadRequest("No _id given in body")
- }
-
- let WorkItem = this.db.WorkItem
- let workItemUpdates = null
-
- try {
- workItemUpdates = new WorkItem(req.body)
- } catch (err) {
- throw createError.BadRequest("Invalid data")
- }
-
- const foundWorkItem = await WorkItem.findById(workItemUpdates._id)
-
- if (!foundWorkItem) {
- return next(
- createError.NotFound(`WorkItem with _id ${_id} was not found`)
- )
- }
-
- foundWorkItem.merge(workItemUpdates)
-
- const savedWorkItem = await foundWorkItem.save()
-
- res.json(savedWorkItem.toClient())
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!isAdmin) {
+ return new createError.Forbidden()
}
+
+ // Do this here because Mongoose will add it automatically otherwise
+ if (!req.body._id) {
+ throw createError.BadRequest("No _id given in body")
+ }
+
+ let WorkItem = this.db.WorkItem
+ let workItemUpdates = null
+
+ try {
+ workItemUpdates = new WorkItem(req.body)
+ } catch (err) {
+ throw createError.BadRequest("Invalid data")
+ }
+
+ const foundWorkItem = await WorkItem.findById(workItemUpdates._id)
+
+ if (!foundWorkItem) {
+ return next(
+ createError.NotFound(`WorkItem with _id ${_id} was not found`)
+ )
+ }
+
+ foundWorkItem.merge(workItemUpdates)
+
+ const savedWorkItem = await foundWorkItem.save()
+
+ res.json(savedWorkItem.toClient())
}
async getWorkItem(req, res, next) {
- try {
- const WorkItem = this.db.WorkItem
- const _id = req.params._id
- const workItem = await WorkItem.findById(_id)
+ const WorkItem = this.db.WorkItem
+ const _id = req.params._id
+ const workItem = await WorkItem.findById(_id)
- if (!workItem) {
- throw createError.NotFound(`WorkItem with _id ${_id} not found`)
- }
-
- res.json(workItem.toClient())
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!workItem) {
+ throw createError.NotFound(`WorkItem with _id ${_id} not found`)
}
+
+ res.json(workItem.toClient())
}
async deleteWorkItem(req, res, next) {
- try {
- const isAdmin = req.user.administrator
+ const isAdmin = req.user.administrator
- if (!isAdmin) {
- return new createError.Forbidden()
- }
-
- const WorkItem = this.db.WorkItem
- const _id = req.params._id
- const workItem = await WorkItem.remove({ _id })
-
- if (!workItem) {
- throw createError.NotFound(`WorkItem with _id ${_id} not found`)
- }
-
- res.json({})
- } catch (err) {
- if (err instanceof createError.HttpError) {
- next(err)
- } else {
- next(createError.InternalServerError(err.message))
- }
+ if (!isAdmin) {
+ return new createError.Forbidden()
}
+
+ const WorkItem = this.db.WorkItem
+ const _id = req.params._id
+ const workItem = await WorkItem.remove({ _id })
+
+ if (!workItem) {
+ throw createError.NotFound(`WorkItem with _id ${_id} not found`)
+ }
+
+ res.json({})
}
}
diff --git a/server/src/api/routes/index.js b/server/src/api/routes/index.js
index 9f20d11..3ff7008 100644
--- a/server/src/api/routes/index.js
+++ b/server/src/api/routes/index.js
@@ -1,6 +1,21 @@
-export { AuthRoutes } from './AuthRoutes'
-export { AssetRoutes } from './AssetRoutes'
-export { UserRoutes } from './UserRoutes'
-export { WorkItemRoutes } from './WorkItemRoutes'
-export { ActivityRoutes } from './ActivityRoutes'
-export { TeamRoutes } from './TeamRoutes'
+export { AuthRoutes } from "./AuthRoutes"
+export { AssetRoutes } from "./AssetRoutes"
+export { UserRoutes } from "./UserRoutes"
+export { WorkItemRoutes } from "./WorkItemRoutes"
+export { ActivityRoutes } from "./ActivityRoutes"
+export { TeamRoutes } from "./TeamRoutes"
+import createError from "http-errors"
+
+export function catchAll(routeHandler) {
+ return (req, res, next) => {
+ try {
+ routeHandler(req, res, next)
+ } catch (err) {
+ if (err instanceof createError.HttpError) {
+ next(err)
+ } else {
+ next(createError.InternalServerError(err.message))
+ }
+ }
+ }
+}
diff --git a/server/src/database/DB.js b/server/src/database/DB.js
index 7c637cf..0b1ed5d 100644
--- a/server/src/database/DB.js
+++ b/server/src/database/DB.js
@@ -11,31 +11,24 @@ Grid.mongo = mongoose.mongo
@autobind
export class DB {
constructor() {
- mongoose.Promise = Promise
mongoose.plugin(merge)
}
- connect(mongoUri, isProduction) {
- return mongoose
- .connect(mongoUri, {
- useMongoClient: true,
- config: { autoIndex: !isProduction },
- })
- .then((connection) => {
- this.connection = connection
+ async connect(mongoUri, isProduction) {
+ const connection = await mongoose.createConnection(mongoUri, {
+ promiseLibrary: Promise,
+ autoIndex: !isProduction,
+ })
- this.gridfs = Grid(connection.db)
- this.gridfs.findOneAsync = util.promisify(this.gridfs.findOne)
- this.gridfs.removeAsync = util.promisify(this.gridfs.remove)
+ this.gridfs = Grid(connection.db)
+ this.gridfs.findOneAsync = util.promisify(this.gridfs.findOne)
+ this.gridfs.removeAsync = util.promisify(this.gridfs.remove)
- this.User = connection.model("User", Schemas.userSchema)
- this.WorkItem = connection.model("WorkItem", Schemas.workItemSchema)
- this.Activity = connection.model("Activity", Schemas.activitySchema)
- this.Team = connection.model("Team", Schemas.teamSchema)
- this.Counter = connection.model("Counter", Schemas.counterSchema)
-
- return Promise.resolve(this)
- })
+ this.User = connection.model("User", Schemas.userSchema)
+ this.WorkItem = connection.model("WorkItem", Schemas.workItemSchema)
+ this.Activity = connection.model("Activity", Schemas.activitySchema)
+ this.Team = connection.model("Team", Schemas.teamSchema)
+ this.Counter = connection.model("Counter", Schemas.counterSchema)
}
newObjectId(s) {
@@ -43,17 +36,16 @@ export class DB {
return new mongodb.ObjectID(s).toString()
}
- lookupToken(token, done) {
- this.User.findOne({ loginToken: token })
- .then((user) => {
- if (!user) {
- done(null, false)
- } else {
- done(null, user)
- }
- })
- .catch((err) => {
- done(err)
- })
+ async lookupToken(token, done) {
+ try {
+ const user = await this.User.findOne({ loginToken: token })
+ if (!user) {
+ done(null, false)
+ } else {
+ done(null, user)
+ }
+ } catch (err) {
+ done(err)
+ }
}
}
diff --git a/server/src/database/schemas/activity.js b/server/src/database/schemas/activity.js
index b85a930..419c182 100644
--- a/server/src/database/schemas/activity.js
+++ b/server/src/database/schemas/activity.js
@@ -1,24 +1,32 @@
-import { Schema } from 'mongoose'
+import { Schema } from "mongoose"
-export let activitySchema = new Schema({
- _id: { type: Schema.Types.ObjectId, required: true, auto: true },
- resolution: String,
- workItem: { type: Schema.Types.ObjectId, required: true },
- status: { type: String, required: true, enum: {
- values: [ 'planned', 'open', 'onHold', 'closed'],
- message: 'enum validator failed for path `{PATH}` with value `{VALUE}`'
- }},
- notes: String,
- when: Date,
- loc: {
- type: { type: String },
- coordinates: [Number],
+export let activitySchema = new Schema(
+ {
+ _id: { type: Schema.Types.ObjectId, required: true, auto: true },
+ resolution: { type: String, required: true },
+ workItem: { type: Schema.Types.ObjectId, required: true },
+ status: {
+ type: String,
+ required: true,
+ enum: {
+ values: ["planned", "open", "onHold", "closed"],
+ message: "enum validator failed for path `{PATH}` with value `{VALUE}`",
+ },
+ required: true,
+ },
+ notes: { type: String, required: true },
+ when: { type: Date, required: true },
+ location: {
+ type: { type: String },
+ coordinates: [Number],
+ },
+ address: String,
+ fromStreetNumber: Number,
+ toStreetNumber: Number,
+ photos: [Schema.Types.ObjectId],
},
- address: String,
- fromStreetNumber: Number,
- toStreetNumber: Number,
- photos: [ Schema.Types.ObjectId ],
-}, { timestamps: true, id: false })
+ { timestamps: true, id: false }
+)
activitySchema.methods.toClient = function() {
return this.toObject()
diff --git a/server/src/database/schemas/workItem.js b/server/src/database/schemas/workItem.js
index d82232c..8d1e289 100644
--- a/server/src/database/schemas/workItem.js
+++ b/server/src/database/schemas/workItem.js
@@ -40,6 +40,13 @@ workItemSchema.pre("save", async function(next) {
next()
})
+workItemSchema.virtual("activities", {
+ ref: "Activity",
+ localField: "_id",
+ foreignField: "workItem",
+ justOne: false,
+})
+
workItemSchema.methods.toClient = function() {
return this.toObject()
}