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() }