SectionList on home screen with API

This commit is contained in:
John Lyon-Smith
2018-04-08 18:33:21 -07:00
parent 5634acb967
commit 7891bb71c9
19 changed files with 1278 additions and 1201 deletions

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -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

View File

@@ -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 (
<View style={styles.container}>
<Header
@@ -161,12 +116,12 @@ export class Home extends React.Component {
latitudeDelta: 0.0922,
longitudeDelta: 0.0922,
}}>
{data.map((marker) => (
{sections.map((section, index) => (
<Marker
key={marker.key}
coordinate={marker.latlng}
title={marker.title}
description={marker.location}
key={index}
coordinate={section.coordinate}
title={workItemTypeText[section.workItemType]}
description={section.address}
image={pinImage}
anchor={{ x: 0.5, y: 1.0 }}
/>
@@ -178,7 +133,7 @@ export class Home extends React.Component {
alignItems: "center",
width: "100%",
height: 40,
backgroundColor: "#F4F4F4",
backgroundColor: "white",
}}>
<Icon
name="search"
@@ -187,7 +142,7 @@ export class Home extends React.Component {
/>
<TextInput
style={{ flexGrow: 1, flexBasis: 0, height: "100%" }}
underlineColorAndroid="#F4F4F4"
underlineColorAndroid="white"
placeholder="Search"
/>
<Icon
@@ -196,12 +151,34 @@ export class Home extends React.Component {
style={{ marginLeft: 5, marginRight: 10, tintColor: "gray" }}
/>
</View>
<FlatList
<SectionList
style={{ width: "100%", flexGrow: 1 }}
data={data}
renderItem={({ item, index }) => {
sections={sections}
renderSectionHeader={({ section: workItem }) => (
<View
key={workItem._id}
style={{
flexDirection: "column",
justifyContent: "center",
backgroundColor: "#F4F4F4",
paddingLeft: 8,
height: 45,
}}>
<Text style={{ fontSize: 16 }}>
WORK ORDER {pad(workItem.ticketNumber, 4)}
</Text>
</View>
)}
renderItem={({ item: activity, section }) => {
return (
<View style={{ flexDirection: "row", height: 50 }}>
<View
key={activity._id}
style={{
flexDirection: "row",
height: 50,
marginTop: 3,
marginBottom: 3,
}}>
<Text
style={{
fontSize: 8,
@@ -209,17 +186,19 @@ export class Home extends React.Component {
marginLeft: 5,
alignSelf: "center",
}}>
{item.state.toUpperCase()}
{activity.status.toUpperCase()}
</Text>
<View style={{ width: "75%", flexDirection: "column" }}>
<Text style={{ fontSize: 20 }}>{item.title}</Text>
<Text style={{ fontSize: 20, fontWeight: "bold" }}>
{activity.resolution}
</Text>
<Text style={{ fontSize: 14, color: "gray" }}>
{item.location}
{activity.address || "..."}
</Text>
</View>
<TouchableOpacity
style={{ alignSelf: "center" }}
onPress={() => this.handleItemSelect(item, index)}>
onPress={() => this.handleItemSelect(activity, index)}>
<Icon name="rightArrow" size={16} />
</TouchableOpacity>
</View>

View File

@@ -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 (
<View style={{ flex: 1 }}>
@@ -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}
/>
<Icon

View File

@@ -13,7 +13,13 @@ import { MessageModal } from "../Modal"
import autobind from "autobind-decorator"
import { SwipeListView } from "react-native-swipe-list-view"
import { api } from "../API"
import { workItemTypeEnum, formatLatLng, parseLatLng, pad } from "../util"
import {
workItemTypeEnum,
workItemTypeText,
formatLatLng,
parseLatLng,
pad,
} from "../util"
const styles = StyleSheet.create({
container: {
@@ -24,11 +30,6 @@ const styles = StyleSheet.create({
},
})
const workItemTypeText = workItemTypeEnum.reduce((result, item) => {
result[item.value] = item.text
return result
}, {})
export class WorkItemList extends React.Component {
constructor(props) {
super(props)

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

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

View File

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

View File

@@ -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 <offset>\''))
return next(
createError.BadRequest(
"Range header must be supplied and of form 'byte <offset>'"
)
)
}
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))
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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