From f7c73ee277e832dcb3315ab106d3ccad35ecb1bf Mon Sep 17 00:00:00 2001 From: John Lyon-Smith Date: Fri, 20 Apr 2018 17:40:55 -0700 Subject: [PATCH] Enabling test server and bug fixes --- mobile/src/Activity/Activity.js | 61 ++++++-- server/config/default-test.json5 | 27 ++++ server/config/default.json5 | 6 + server/config/production-test.json5 | 5 + server/deighton-ar-test.service | 16 ++ server/src/api/index.js | 5 +- server/src/api/routes/SystemRoutes.js | 23 +++ server/src/api/routes/index.js | 1 + server/src/email/index.js | 50 ++++--- server/src/image/index.js | 56 +++---- server/src/server.js | 36 +++-- server/src/version.js | 7 + version.json5 | 1 + website/src/Users/UserForm.js | 201 ++++++++++++++++++++------ website/src/Users/Users.js | 30 ++++ 15 files changed, 399 insertions(+), 126 deletions(-) create mode 100644 server/config/default-test.json5 create mode 100644 server/config/production-test.json5 create mode 100644 server/deighton-ar-test.service create mode 100644 server/src/api/routes/SystemRoutes.js create mode 100644 server/src/version.js diff --git a/mobile/src/Activity/Activity.js b/mobile/src/Activity/Activity.js index f554fb0..ffbfd4f 100644 --- a/mobile/src/Activity/Activity.js +++ b/mobile/src/Activity/Activity.js @@ -51,24 +51,26 @@ const styles = StyleSheet.create({ export class Activity extends React.Component { static bindings = { - dateTime: { - isValid: true, + header: { + noValue: true, + isDisabled: (r) => !(r.anyModified && r.allValid), }, - location: { + dateTime: { isValid: (r, v) => v !== "", isReadOnly: true, }, details: { - isValid: true, + isValid: (r, v) => v !== "", }, resolution: { - isValid: true, + isValid: (r, v) => v !== "", }, notes: { - isValid: true, + isValid: (r, v) => v !== "", }, status: { - isValid: true, + isValid: (r, v) => v !== "", + alwaysGet: true, }, } @@ -136,14 +138,55 @@ export class Activity extends React.Component { } } + @autobind + handleDonePress() { + const { binder } = this.state + let obj = binder.getModifiedFieldValues() + + if (!obj._id) { + api + .createActivity(obj) + .then((activity) => { + this.handleBackPress() + }) + .catch((error) => { + this.setState({ + messageModal: { + icon: "hand", + message: "Unable to create activity", + detail: error.message, + }, + }) + }) + } else { + api + .updateActivity(obj) + .then((activity) => { + this.handleBackPress() + }) + .catch((error) => { + this.setState({ + messageModal: { + icon: "hand", + message: "Unable to update activity", + detail: error.message, + }, + }) + }) + } + } + render() { const { binder, messageModal, region } = this.state return ( -
diff --git a/server/config/default-test.json5 b/server/config/default-test.json5 new file mode 100644 index 0000000..1f4644b --- /dev/null +++ b/server/config/default-test.json5 @@ -0,0 +1,27 @@ +{ + logDir: '', + serviceName: { + server: 'dar-test-server', + api: 'dar-test-api', + email: 'dar-test-email', + image: 'dar-test-image', + }, + uri: { + mongo: 'mongodb://localhost/dar-test-v1', + amqp: 'amqp://localhost', + redis: 'redis://localhost', + }, + api: { + port: '3002', + loginKey: '6508b427b3cc486498671cfd7967218bd6b95fbb42f5e17a112f4c9e9900057c', + uploadTimout: 3600, + }, + email: { + senderEmail: 'support@kss.us.com', + maxEmailTokenAgeInHours: 36, + maxPasswordTokenAgeInHours: 3, + sendEmailDelayInSeconds: 3, + supportEmail: 'support@kss.us.com', + sendEmail: true + } +} diff --git a/server/config/default.json5 b/server/config/default.json5 index b6cb375..a0ca52a 100644 --- a/server/config/default.json5 +++ b/server/config/default.json5 @@ -1,5 +1,11 @@ { logDir: '', + serviceName: { + server: 'dar-server', + api: 'dar-api', + email: 'dar-email', + image: 'dar-image', + }, uri: { mongo: 'mongodb://localhost/dar-v1', amqp: 'amqp://localhost', diff --git a/server/config/production-test.json5 b/server/config/production-test.json5 new file mode 100644 index 0000000..92b2321 --- /dev/null +++ b/server/config/production-test.json5 @@ -0,0 +1,5 @@ +{ + api: { + port: '3006', + }, +} diff --git a/server/deighton-ar-test.service b/server/deighton-ar-test.service new file mode 100644 index 0000000..b99a74e --- /dev/null +++ b/server/deighton-ar-test.service @@ -0,0 +1,16 @@ +[Unit] +Description=Deighton AR Test Service +After=rabbitmq-server.service mongod.service redis-server.service + +[Service] +Type=simple +User=ubuntu +Group=ubuntu +WorkingDirectory=/home/ubuntu/deighton-ar/server +Environment='NODE_ENV=production' +Environment='NODE_APP_INSTANCE=test' +ExecStart=/usr/bin/node dist/server.js +Restart=on-abort + +[Install] +WantedBy=multi-user.target diff --git a/server/src/api/index.js b/server/src/api/index.js index 9ce6637..7ca5925 100644 --- a/server/src/api/index.js +++ b/server/src/api/index.js @@ -19,8 +19,8 @@ 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 = config.get("serviceName.api") +const isProduction = process.env.NODE_ENV === "production" let log = null if (isProduction) { @@ -71,6 +71,7 @@ Promise.all([ log.info(`Connected to Redis at ${redisUri}`) try { + container.systemRoutes = new Routes.SystemRoutes(container) container.authRoutes = new Routes.AuthRoutes(container) container.userRoutes = new Routes.UserRoutes(container) container.assetRoutes = new Routes.AssetRoutes(container) diff --git a/server/src/api/routes/SystemRoutes.js b/server/src/api/routes/SystemRoutes.js new file mode 100644 index 0000000..a961607 --- /dev/null +++ b/server/src/api/routes/SystemRoutes.js @@ -0,0 +1,23 @@ +import createError from "http-errors" +import autobind from "autobind-decorator" +import { catchAll } from "." +import { versionInfo } from "../../version" +import { version } from "urlsafe-base64/lib/urlsafe-base64" + +@autobind +export class SystemRoutes { + constructor(container) { + const app = container.app + + this.log = container.log + + app.route("/system/version").get(this.getVersion) + } + + async getVersion(req, res, next) { + const { fullVersion } = versionInfo + res.json({ + fullVersion, + }) + } +} diff --git a/server/src/api/routes/index.js b/server/src/api/routes/index.js index cb3741a..239553c 100644 --- a/server/src/api/routes/index.js +++ b/server/src/api/routes/index.js @@ -4,6 +4,7 @@ export { UserRoutes } from "./UserRoutes" export { WorkItemRoutes } from "./WorkItemRoutes" export { ActivityRoutes } from "./ActivityRoutes" export { TeamRoutes } from "./TeamRoutes" +export { SystemRoutes } from "./SystemRoutes" import createError from "http-errors" export function catchAll(routeHandler) { diff --git a/server/src/email/index.js b/server/src/email/index.js index 69ef754..7a0f3d7 100644 --- a/server/src/email/index.js +++ b/server/src/email/index.js @@ -1,19 +1,20 @@ -import config from 'config' -import pino from 'pino' -import * as pinoExpress from 'pino-pretty-express' -import createError from 'http-errors' -import { MS } from '../message-service' -import { EmailHandlers } from './EmailHandlers' -import path from 'path' -import fs from 'fs' +import config from "config" +import pino from "pino" +import * as pinoExpress from "pino-pretty-express" +import createError from "http-errors" +import { MS } from "../message-service" +import { EmailHandlers } from "./EmailHandlers" +import path from "path" +import fs from "fs" -const serviceName = 'dar-email' -const isProduction = (process.env.NODE_ENV == 'production') +const serviceName = config.get("serviceName.email") +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({}) @@ -24,17 +25,20 @@ if (isProduction) { const ms = new MS(serviceName, { durable: true }, log) let container = { ms, log } -const amqpUri = config.get('uri.amqp') +const amqpUri = config.get("uri.amqp") -ms.connect(amqpUri).then(() => { - log.info(`Connected to RabbitMQ at ${amqpUri}`) +ms + .connect(amqpUri) + .then(() => { + log.info(`Connected to RabbitMQ at ${amqpUri}`) - container = { - ...container, - handlers: new EmailHandlers(container) - } + container = { + ...container, + handlers: new EmailHandlers(container), + } - ms.listen(container.handlers) -}).catch((err) => { - log.error(isProduction ? err.message : err) -}) + ms.listen(container.handlers) + }) + .catch((err) => { + log.error(isProduction ? err.message : err) + }) diff --git a/server/src/image/index.js b/server/src/image/index.js index 16369aa..5b72464 100644 --- a/server/src/image/index.js +++ b/server/src/image/index.js @@ -1,19 +1,20 @@ -import config from 'config' -import pino from 'pino' -import * as pinoExpress from 'pino-pretty-express' -import { DB } from '../database' -import { MS } from '../message-service' -import { ImageHandlers } from './ImageHandlers' -import path from 'path' -import fs from 'fs' +import config from "config" +import pino from "pino" +import * as pinoExpress from "pino-pretty-express" +import { DB } from "../database" +import { MS } from "../message-service" +import { ImageHandlers } from "./ImageHandlers" +import path from "path" +import fs from "fs" -const serviceName = 'dar-image' -const isProduction = (process.env.NODE_ENV == 'production') +const serviceName = config.get("serviceName.image") +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({}) @@ -24,22 +25,21 @@ if (isProduction) { const ms = new MS(serviceName, { durable: false }, log) const db = new DB() let container = { db, ms, log } -const mongoUri = config.get('uri.mongo') -const amqpUri = config.get('uri.amqp') +const mongoUri = config.get("uri.mongo") +const amqpUri = config.get("uri.amqp") -Promise.all([ - db.connect(mongoUri), - ms.connect(amqpUri) -]).then(() => { - log.info(`Connected to MongoDB at ${mongoUri}`) - log.info(`Connected to RabbitMQ at ${amqpUri}`) +Promise.all([db.connect(mongoUri), ms.connect(amqpUri)]) + .then(() => { + log.info(`Connected to MongoDB at ${mongoUri}`) + log.info(`Connected to RabbitMQ at ${amqpUri}`) - container = { - ...container, - handlers: new ImageHandlers(container) - } + container = { + ...container, + handlers: new ImageHandlers(container), + } - ms.listen(container.handlers) -}).catch((err) => { - log.error(isProduction ? err.message : err) -}) + ms.listen(container.handlers) + }) + .catch((err) => { + log.error(isProduction ? err.message : err) + }) diff --git a/server/src/server.js b/server/src/server.js index a0452e9..bb672f4 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -1,18 +1,19 @@ #!/usr/bin/env node -import { ServerTool } from './ServerTool' -import pino from 'pino' -import * as pinoExpress from 'pino-pretty-express' -import path from 'path' -import fs from 'fs' -import config from 'config' +import { ServerTool } from "./ServerTool" +import pino from "pino" +import * as pinoExpress from "pino-pretty-express" +import path from "path" +import fs from "fs" +import config from "config" -const serviceName = 'dar-server' -const isProduction = (process.env.NODE_ENV == 'production') +const serviceName = config.get("serviceName.server") +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({}) @@ -20,10 +21,13 @@ if (isProduction) { log = pino({ name: serviceName }, pretty) } -const tool = new ServerTool(path.basename(process.argv[1], '.js'), log) +const tool = new ServerTool(path.basename(process.argv[1], ".js"), log) -tool.run(process.argv.slice(2)).then((exitCode) => { - process.exitCode = exitCode -}).catch((err) => { - console.error(err) -}) +tool + .run(process.argv.slice(2)) + .then((exitCode) => { + process.exitCode = exitCode + }) + .catch((err) => { + console.error(err) + }) diff --git a/server/src/version.js b/server/src/version.js new file mode 100644 index 0000000..c66fcc9 --- /dev/null +++ b/server/src/version.js @@ -0,0 +1,7 @@ +export const versionInfo = { + version: '1.0.0', + fullVersion: '1.0.0-20180415.0', + title: 'Deighton AR System', + copyright: '© 2018, Kingston Software Solutions.', + supportEmail: 'support@kss.us.com', +} diff --git a/version.json5 b/version.json5 index 8c10be9..bb857a6 100644 --- a/version.json5 +++ b/version.json5 @@ -7,6 +7,7 @@ "mobile/ios/DeightonAR/info.plist", "mobile/android/app/build.gradle", "mobile/android/app/src/main/AndroidManifest.xml", + "service/src/version.js", "scratch/version.txt", "scratch/version.tag.txt", "scratch/version.desc.txt" diff --git a/website/src/Users/UserForm.js b/website/src/Users/UserForm.js index b56230a..f09270d 100644 --- a/website/src/Users/UserForm.js +++ b/website/src/Users/UserForm.js @@ -1,13 +1,19 @@ -import React from 'react' -import PropTypes from 'prop-types' -import autobind from 'autobind-decorator' -import { regExpPattern } from 'regexp-pattern' -import { api } from 'src/API' +import React from "react" +import PropTypes from "prop-types" +import autobind from "autobind-decorator" +import { regExpPattern } from "regexp-pattern" +import { api } from "src/API" import { - Row, Column, BoundInput, BoundButton, BoundCheckbox, BoundEmailIcon, BoundDropdown, -} from 'ui' -import { FormBinder } from 'react-form-binder' -import { sizeInfo } from 'ui/style' + Row, + Column, + BoundInput, + BoundButton, + BoundCheckbox, + BoundEmailIcon, + BoundDropdown, +} from "ui" +import { FormBinder } from "react-form-binder" +import { sizeInfo } from "ui/style" export class UserForm extends React.Component { static propTypes = { @@ -16,63 +22,72 @@ export class UserForm extends React.Component { onRemove: PropTypes.func, onModifiedChanged: PropTypes.func, onChangeEmail: PropTypes.func, - onResendEmail: PropTypes.func + onResendEmail: PropTypes.func, + onResetPassword: PropTypes.func, } static bindings = { email: { - isValid: (r, v) => (regExpPattern.email.test(v)), - isDisabled: (r) => (r._id) + isValid: (r, v) => regExpPattern.email.test(v), + isDisabled: (r) => r._id, }, emailValidated: { initValue: false, - isDisabled: (r) => (!r._id) + isDisabled: (r) => !r._id, + }, + resetPassword: { + noValue: true, + isDisabled: (r) => !r._id || api.loggedInUser._id === r._id, }, changeEmail: { noValue: true, - isDisabled: (r) => (!r._id) + isDisabled: (r) => !r._id, }, resendEmail: { noValue: true, - isDisabled: (r) => (!r._id || !!r.getFieldValue('emailValidated')) + isDisabled: (r) => !r._id || !!r.getFieldValue("emailValidated"), }, firstName: { - isValid: (r, v) => (v !== '') + isValid: (r, v) => v !== "", }, lastName: { - isValid: (r, v) => (v !== '') + isValid: (r, v) => v !== "", }, team: { - isValid: true + isValid: true, }, administrator: { isValid: (r, v) => true, initValue: false, - isDisabled: (r) => (api.loggedInUser._id === r._id), // Adding a new user + isDisabled: (r) => api.loggedInUser._id === r._id, // Adding a new user alwaysGet: true, }, remove: { noValue: true, - isVisible: (r) => (r._id), - isDisabled: (r) => (api.loggedInUser._id === r._id) + isVisible: (r) => r._id, + isDisabled: (r) => api.loggedInUser._id === r._id, }, reset: { noValue: true, isDisabled: (r) => { return !r.anyModified - } + }, }, submit: { noValue: true, - isDisabled: (r) => (!r.anyModified || !r.allValid), + isDisabled: (r) => !r.anyModified || !r.allValid, }, } constructor(props) { super(props) this.state = { - binder: new FormBinder(props.user, UserForm.bindings, props.onModifiedChanged), - teams: [] + binder: new FormBinder( + props.user, + UserForm.bindings, + props.onModifiedChanged + ), + teams: [], } this.getTeams() @@ -81,17 +96,28 @@ export class UserForm extends React.Component { // TODO: This is not very efficient. Better to get the teams in User.js and pass them in // This however will always be up-to-date. Need to use the WebSocket to refresh. getTeams() { - api.listTeams().then((list) => { - this.setState({ teams: list.items.map((item) => ({ value: item._id, text: item.name, icon: 'team' })).sort((a, b) => (a.text.localeCompare(b.text))) }) - }).catch(() => { - this.setState({ teams: [] }) - }) + api + .listTeams() + .then((list) => { + this.setState({ + teams: list.items + .map((item) => ({ value: item._id, text: item.name, icon: "team" })) + .sort((a, b) => a.text.localeCompare(b.text)), + }) + }) + .catch(() => { + this.setState({ teams: [] }) + }) } componentWillReceiveProps(nextProps) { if (nextProps.user !== this.props.user) { this.setState({ - binder: new FormBinder(nextProps.user, UserForm.bindings, nextProps.onModifiedChanged) + binder: new FormBinder( + nextProps.user, + UserForm.bindings, + nextProps.onModifiedChanged + ), }) this.getTeams() @@ -113,7 +139,9 @@ export class UserForm extends React.Component { handleReset() { const { user, onModifiedChanged } = this.props - this.setState({ binder: new FormBinder(user, UserForm.bindings, onModifiedChanged) }) + this.setState({ + binder: new FormBinder(user, UserForm.bindings, onModifiedChanged), + }) if (onModifiedChanged) { onModifiedChanged(false) @@ -122,19 +150,37 @@ export class UserForm extends React.Component { @autobind handleChangeEmail() { - this.props.onChangeEmail() + const { onChangeEmail } = this.props + + if (onChangeEmail) { + onChangeEmail() + } } @autobind handleResendEmail() { - this.props.onResendEmail() + const { onResendEmail } = this.props + if (onResendEmail) { + onResendEmail() + } + } + + @autobind + handleResetPassword() { + const { onResetPassword } = this.props + if (onResetPassword) { + onResetPassword() + } } render() { const { binder, teams } = this.state return ( -
+ @@ -145,39 +191,79 @@ export class UserForm extends React.Component { - + - + - + - + - + - + - + + + + + @@ -185,7 +271,11 @@ export class UserForm extends React.Component { - + @@ -194,15 +284,30 @@ export class UserForm extends React.Component { - + - + - + diff --git a/website/src/Users/Users.js b/website/src/Users/Users.js index 2181b70..1ffa958 100644 --- a/website/src/Users/Users.js +++ b/website/src/Users/Users.js @@ -148,6 +148,35 @@ export class Users extends Component { }) } + @autobind + handleSendPasswordReset() { + this.setState({ waitModal: "Sending Password Reset Email..." }) + api + .sendResetPassword(this.state.selectedUser.email) + .then(() => { + this.setState({ + waitModal: null, + messageModal: { + icon: "thumb", + message: `An email has been sent to '${ + this.state.selectedUser.email + }' with instructions on how to reset their password`, + }, + }) + }) + .catch((error) => { + this.setState({ + error: true, + waitModal: null, + messageModal: { + icon: "hand", + message: "Unable to request password reset.", + detail: error.message, + }, + }) + }) + } + @autobind handleResendEmail() { this.setState({ @@ -339,6 +368,7 @@ export class Users extends Component { onModifiedChanged={this.handleModifiedChanged} onChangeEmail={this.handleChangeEmail} onResendEmail={this.handleResendEmail} + onResetPassword={this.handleSendPasswordReset} /> ) : (