Ready to go

This commit is contained in:
Luke Berndt
2021-02-15 21:56:19 -05:00
parent b78f47f0fa
commit 5ecc438ffe
312 changed files with 122 additions and 73935 deletions

141
README.md
View File

@@ -28,47 +28,66 @@ There are a lot of different components that make up the system. All of the serv
## Operations
You can run things in 2 different modes, `test` and `prod`. The big difference is that `prod` expects all of the calls to be https and gets cranky when they are not. I have gotten `test` to run fine on my laptop, so that is probably a good starting point.
You can run things in 2 different modes, **test** and **prod**. The big difference is that **prod** expects all of the calls to be https and gets cranky when they are not. I have gotten **test** to run fine on my laptop, so that is probably a good starting point.
Currently, both `test` and `prod` expect to use S3-based storage instead of local storage. Switching to use local storage would be relatively easy - but for the sake of testing, let's just say use something S3-compatible. Make sure that ~/.aws/credentials has the credentials you'd like to use with your S3-compatible storage provider.. IE:
### DNS Entries
You should have a domain name pointing to the IP address of the server you are going to use.
CNAMEs also need to be created for the various services. Create the CNAMEs below with your DNS host:
- api
- account
- admin
After doing this, you should have the following domains: `api.domain.com`, `account.domain.com`, `admin.domain.com`
### S3 Storage
Currently, both **test** and **prod** expect to use S3-based storage instead of local storage. Switching to use local storage would be relatively easy - but for the sake of testing, let's just say use something S3-compatible. Make sure that ~/.aws/credentials has the credentials you'd like to use with your S3-compatible storage provider.. IE:
```
[default]
aws_access_key_id = [..]
aws_secret_access_key = [..]
```
Copy docker-test.sh.example to docker-test.sh, and/or docker-prod.sh.example to docker-prod.sh.
### Configure
The configurations for both **test** and **prod** come from environment variable files that are read in before the containers are started.
Copy the example files and fill in the required info:
```bash
cp test.env.example test.env
cp prod.env.example prod.env
```
Fill in:
- MailJet information
- S3 information
- Site name
- Admin Email (The email will appear to come from this email. You should make sure it matches the domain MailJet is configured for)
- How many days calls will be archived for.... this is just a UI thing, you need to create some S3 rules to make sure they are deleted
### Scripts
`./docker-test.sh` - sets up docker-compose to run with the correct environment variables for testing
`./docker-prod.sh` - sets up docker-compose to run with the correct environment variables for production
Fill out the Environment variables in both these files. S3_PROFILE should match the profile that is defined in the aws/credentials file. You don't need to worry about the Stripe stuff, since that is dormant.
If you are doing frontend work, you can turn off the de-obfuscation. To do this, edit the Dockerfile for the server you are working with. Change the CMD to:
`CMD ["npm", "run", "start:dev"]` Just remember to turn it off when you make the code public.
### Hosting
I use Digital Ocean and they have been pretty great. If you need hosting, give them a try and use my referal code: https://m.do.co/c/402fa446f7a6
### Service I Like
- 💻 I use Digital Ocean and they have been pretty great. If you need hosting, give them a try and use my referal code: https://m.do.co/c/402fa446f7a6
You get $100 of credit to use in 60 days.
If you need storage, give Wasabi a try. The have been mostly reliable and you can't beat the price: https://wasabi.com
- 💾 If you need storage, give Wasabi a try. The have been mostly reliable and you can't beat the price: https://wasabi.com
- 📨 I use Mailjet... and you need to also. It makes it easy to send out email address confirmations: https://www.mailjet.com
- 🔒 I use Let's Encrypt. It works.
### HTTPS
I use Let's Encrypt. It works.
## Local Testing
### For Local Testing:
- In root dir, run: `./docker-test.sh build`
- `./docker-test.sh up -d`
### Local DNS
You can then browser to:
- openmhz.test
- admin.openmhz.test
It helps to have similar subdomains mapping to localhost/127.0.0.1. I used the domain `openmhz.test`. Feel free to use this, or come up with something clever. If you do something different, make sure you use that instead in the commands below.
Make sure the following is in: */etc/hosts*
On MacOS, make the changes in: */etc/hosts*
````
##
@@ -80,23 +99,15 @@ Make sure the following is in: */etc/hosts*
127.0.0.1 localhost
127.0.0.1 openmhz.test api.openmhz.test admin.openmhz.test media.openmhz.test
````
### Default user name, for Test:
- test@test.com
- password: password
Then load these values into local DNS: `dscacheutil –flushcache`
### DNS Entries
CNAMEs need to be created for the various services. Create the CNAMEs below with your DNS host:
- api
- account
- media
- admin
After doing this, you should have the following domains: api.domain.com, account.domain.com, media.domain.com, admin.domain.compose
- In root dir, run: `./docker-test.sh build`
- `./docker-test.sh up -d`
You can then browser to:
- openmhz.test
- admin.openmhz.test
### Debugging React Apps using Hot Reloading
If you are trying to make changes to any of react frontends, it is a huge pain to have to compile to site and rebuild the container each time you make a change. Instead, simply run the react app in development mode. This will work for the frontends for the:
@@ -109,24 +120,20 @@ First, start up all of the containers as described above. You will still the bac
Now go into the respective sub directory for the component you are interested in and run:
````
source ../test.env
yarn install #only need to do this the first time, it installs the Node packages locally
yarn start
````
This should build the frontend and open a browser. In order to have all the cookies work correctly, you have to use the same domain name. Make sure you have setup the local domains as described above. Then goto the base domain, for me that is openmhz.test, at port 3000 `openmhz.test:3000`
### Mailjet
Sign up for MailJet!
https://www.mailjet.com
### Managing MongoDB
## Managing MongoDB
MongoDB is used in the backend to store data. It is pretty fast, flexible and has worked well enough for me.
All of the files that MongoDB uses to store the DB are in the */data* directory, which gets mapped into the contianer.
Mapping this directory makes sure that the data persists each time you run the mongo container.
#### Working with the MongoDB Container
### Working with the MongoDB Container
From the Host OS run:
```bash
@@ -137,9 +144,7 @@ There are a few scripts included with the container:
- **clean.js** This script removes all Calls that are over 30 days old
- **totals.js** Lists different system stats
#### Compact a collection
### Compact a collection
When you run clean.js it doesn't actually remove the files off storge. You can use this command from the mongo cli tool.
First, launch the tool: `mongo`
@@ -149,10 +154,48 @@ And then the compact command on the **Calls** collection:
```
db.runCommand({compact:'calls'})
```
This blocks all calls to the DB, so the site will not work while this is being run.
This blocks all calls to the DB
### Add an Index
Adding an index will make it quicker to search calls by date, talkgroup and whether there are stars.
First, launch the tool: `mongo`
Then switch to the scanner db: `use scanner`
And then add an index:
```
db.calls.createIndex( {shortName: 1, time: -1, star: -1, talkgroupNum: 1})
```
### Upgrading MongoDB
It is a huge pain to upgrade MongoDB in place. It turns out to be easier to dump backup of the database, wipe everything out and then restore into a new database for the latest version of Mongo.
Rough Playbook (use common sense, I may not have this exact):
- get into the shell of the mongo container
- `mongodump --db scanner —out /data/db/backup`
- exit contianer and go back to host machine
- `cd data/db`
- `rm *` erase everything... but not the sub-directories because that is where the backup is
- upgrade to the latest version of mongo
- build and launch the mongo container, which will create an empty DB
- get into the shell of the mongo container
- `mongorestore --db scanner --drop /data/db/backup/`
## Setting up Logging
I have had good luck with Loggly. Their free tier provides enough capabilities for most small sites.
The Doocker Logging Driver works well and is easy to install:
https://documentation.solarwinds.com/en/Success_Center/loggly/Content/admin/docker-logging-driver.htm
### Move a DB
## Move a site to a new server
### Setup aws credentials
Here is the general list of things to do:
- Copy over ~/.aws
- Copy over ~/.secrets
- Do a mongodump on old machine
- Scp the mongo backup to the new machine
- Launch the MongoDB container on the new machine
- Do a mongorestore
- Launch all the conatiners
- Change the Floating IP to point to the new machine

View File

@@ -1,12 +0,0 @@
{
"presets":
["@babel/preset-env",
"@babel/react"],
"plugins": [
"@babel/plugin-proposal-class-properties",
["@babel/plugin-transform-runtime", {
"regenerator": true
}],
["lodash", { "id": ["lodash", "semantic-ui-react"] }]
]
}

View File

@@ -1,3 +0,0 @@
node_modules
lib
public

View File

@@ -1,37 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
lib/
uploads/
# Dependency directory
node_modules
assets
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history

View File

@@ -1,8 +0,0 @@
import { configure } from '@storybook/react';
function loadStories() {
require('../src/client/stories/index.js');
// You can require as many stories as you need.
}
configure(loadStories, module);

View File

@@ -1,21 +0,0 @@
# docker build -t trunk-server-frontend .
# do a `git pull` in trunk-server-frontend to update
FROM node:8.11
ENV HOME=/home/app
RUN mkdir -p /home/app
COPY ./package.json /tmp
RUN cd /tmp && npm install
RUN cp -a /tmp/node_modules /home/app
# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/app/node_modules/.bin:$PATH
WORKDIR $HOME
COPY . $HOME/
RUN npm run build
CMD ["npm", "run", "start"]

View File

@@ -1,10 +0,0 @@
/*
Called during development only. Allows us to use ES6 and JSX in our server code.
*/
require('@babel/register')({
presets: ["@babel/preset-env",
"@babel/react"]
});
require("./src/server/index")

File diff suppressed because it is too large Load Diff

View File

@@ -1,90 +0,0 @@
{
"name": "react-passport-redux-example",
"version": "1.0.0",
"description": "Demonstration of PassportJS authentication in React w/ Redux.",
"main": "devStart.js",
"scripts": {
"build:lib": "babel src/server --out-dir lib/server",
"build:umd": "webpack --config webpack/webpack.config.dev.js",
"build:umd:min": "webpack --config webpack/webpack.config.prod.js",
"build": "npm run clean && npm run build:lib && npm run build:umd && npm run build:umd:min",
"postinstall": "npm run build",
"clean": "rimraf lib public/assets",
"server": "better-npm-run server",
"server:dev": "better-npm-run server:dev",
"start": "npm run server",
"start:dev": "npm run server:dev",
"storybook": "start-storybook -p 9001 -c .storybook"
},
"betterScripts": {
"server": {
"command": "node lib/server/index.js",
"env": {
"NODE_ENV": "production",
"MIN_EXT": ".min"
}
},
"server:dev": {
"command": "node devStart.js",
"env": {
"NODE_ENV": "development"
}
}
},
"dependencies": {
"@babel/runtime": "7.3.1",
"axios": "0.18.0",
"bcrypt-nodejs": "0.0.3",
"better-npm-run": "0.1.1",
"body-parser": "1.18.3",
"connect-mongo": "2.0.3",
"connected-react-router": "6.3.1",
"csv-parse": "4.3.3",
"d3-time-format": "2.1.3",
"express": "4.16.4",
"express-session": "1.15.6",
"fs-extra": "7.0.1",
"lodash": "4.17.11",
"mongoose": "5.4.15",
"multer": "1.4.1",
"node-mailjet": "3.3.1",
"node-uuid": "1.4.8",
"passport": "0.4.0",
"passport-local": "1.0.0",
"react": "16.8.3",
"react-dom": "16.8.3",
"react-redux": "6.0.1",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
"react-router-redux": "5.0.0-alpha.9",
"react-stripe-elements": "2.0.3",
"redux": "4.0.1",
"redux-thunk": "2.3.0",
"semantic-ui-css": "2.4.1",
"semantic-ui-react": "0.85.0",
"storybook": "1.0.0",
"stripe": "6.25.1"
},
"devDependencies": {
"@babel/cli": "7.2.3",
"@babel/core": "7.3.3",
"@babel/plugin-proposal-class-properties": "^7.3.0",
"@babel/plugin-proposal-export-default-from": "7.2.0",
"@babel/plugin-proposal-object-rest-spread": "^7.3.1",
"@babel/plugin-transform-runtime": "7.2.0",
"@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0",
"@babel/preset-stage-1": "^7.0.0",
"@babel/register": "^7.0.0",
"@storybook/react": "4.1.13",
"babel-loader": "8.0.5",
"babel-plugin-lodash": "3.3.4",
"detect-node": "^2.0.4",
"redux-logger": "3.0.6",
"rimraf": "2.6.3",
"webpack": "4.29.5",
"webpack-cli": "3.2.3",
"webpack-dev-middleware": "^3.5.1",
"webpack-hot-middleware": "^2.24.3"
}
}

View File

@@ -1,140 +0,0 @@
import React, { Component } from "react";
import {
Container,
Divider,
Checkbox,
Button,
Header,
Table, Form
} from "semantic-ui-react";
// ----------------------------------------------------
function planPrice(planType) {
switch(planType) {
case freePlanValue:
return freePlanPrice;
case proPlanValue:
return proPlanPrice;
}
}
class ListPlans extends Component {
constructor(props) {
super(props);
this.handleCancel = this.handleCancel.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
var plans = {};
var prices = {};
var total = 0;
this.props.systems.forEach(system => {
plans[system.shortName] = system.planType;
prices[system.shortName] = planPrice(system.planType);
total += planPrice(system.planType);
});
this.state = { plans: plans, prices: prices, total: total };
}
handleCancel() {
var obj = {}
this.props.systems.forEach(system => {
obj[system.shortName] = system.planType;
});
this.setState({ plans: obj });
}
handleUpdate() {
this.props.onUpdate(this.state.plans);
}
componentDidMount() {
}
handleInputChange(e, { name, value }) {
var plans = this.state.plans;
var prices = this.state.prices;
var total = 0;
plans[name] = value;
prices[name] = planPrice(value);
for (var system in prices) {
total += prices[system];
}
this.setState({ plans: plans, prices: prices, total: total });
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
return (
<div>
<Table definition>
<Table.Header>
<Table.Row>
<Table.HeaderCell />
<Table.HeaderCell textAlign='center'>Free</Table.HeaderCell>
<Table.HeaderCell textAlign='center'>Pro</Table.HeaderCell>
<Table.HeaderCell textAlign='center'>Monthly Price</Table.HeaderCell>
</Table.Row>
</Table.Header>
<tbody>
{this.props.systems.map((system) =>
<Table.Row key={system.shortName}>
<Table.Cell>
<Header as='h4'>
<Header.Content>
{system.name}
<Header.Subheader>{system.shortName}</Header.Subheader>
</Header.Content>
</Header>
</Table.Cell>
<Table.Cell textAlign='center'>
<Checkbox
radio
value={0}
name={system.shortName}
checked={this.state.plans[system.shortName] === 0}
onChange={this.handleInputChange}
/>
</Table.Cell>
<Table.Cell textAlign='center'>
<Checkbox
radio
value={10}
name={system.shortName}
checked={this.state.plans[system.shortName] === 10}
onChange={this.handleInputChange}
/>
</Table.Cell>
<Table.Cell textAlign='center'>${this.state.prices[system.shortName]}</Table.Cell>
</Table.Row>
)}
<Table.Row key="total">
<Table.Cell></Table.Cell>
<Table.Cell></Table.Cell>
<Table.Cell textAlign='right'>Estimated Monthly Total:</Table.Cell>
<Table.Cell textAlign='center'>${this.state.total}</Table.Cell>
</Table.Row>
</tbody>
</Table>
<Button.Group floated="right">
<Button content='Cancel' onClick={this.handleCancel} />
<Button content='Update' onClick={this.handleUpdate} primary />
</Button.Group>
</div>
);
}
}
export default ListPlans;

View File

@@ -1,131 +0,0 @@
import React, { Component } from "react";
import {
Container,
Divider,
Modal,
Button,
Segment,
Icon,
Link,
Header, Form, List} from "semantic-ui-react";
import ListPlans from "./ListPlans"
function planName(planType) {
switch(planType) {
case freePlanValue:
return "Free";
case proPlanValue:
return "Pro";
}
}
// ----------------------------------------------------
class UpdatePlans extends Component {
constructor(props) {
super(props);
this.handleUpdate = this.handleUpdate.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.findPlanChanges = this.findPlanChanges.bind(this);
}
state = { modalOpen: false, updatedPlans: {}, changes: [] }
showModal = () => this.setState({ modalOpen: true })
closeModal = () => this.setState({ modalOpen: false })
findPlanChanges(plans) {
var changes = [];
const systems = this.props.system.items;
systems.forEach(system => {
const newPlan = plans[system.shortName];
if (newPlan != system.planType) {
changes.push( system.name + " - " + planName(newPlan));
}
});
return changes;
}
handleOk(){
this.props.updatePlan(this.state.updatedPlans);
this.closeModal();
}
handleCancel() {
this.setState({updatedPlans: {}});
this.closeModal();
}
handleUpdate(plans) {
const changes = this.findPlanChanges(plans);
if (Array.isArray(changes) && changes.length) {
var result = Object.keys(plans).map(function(key) {
return {shortName: key, planType: plans[key]};
});
this.setState({updatedPlans: result, changes: changes});
this.showModal()
}
}
componentDidMount() {
this.props.fetchSystems();
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const systems = this.props.system.items;
const billingDefined = this.props.user.billing;
return (
<div>
<Container text>
<Modal size="tiny" open={this.state.modalOpen} onClose={this.closeModal}>
<Modal.Header>Update Plans</Modal.Header>
<Modal.Content>
<Header as='h4'>The following changes will be made:</Header>
<List as='ul'>
{this.state.changes.map((change) =>
<List.Item as='li' content={change}/>
)}
</List>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleCancel} >
Cancel
</Button>
<Button onClick={this.handleOk} primary>
OK
</Button>
</Modal.Actions>
</Modal>
<Form>
<Header as="h1">System Plans</Header>
<Divider />
{billingDefined? (
<ListPlans systems={systems} onUpdate={this.handleUpdate} key={systems.length}/>
) : (
<Segment>
<Header icon>
<Icon name='credit card outline' />
You need to add a Payment Method before you can upgrade a Plan
</Header>
<Button primary
to='/billing'>Add Payment Method</Button>
</Segment>
)}
</Form>
</Container>
</div>
);
}
}
export default UpdatePlans;

View File

@@ -1,19 +0,0 @@
import { connect } from "react-redux"
import * as systemActions from "../System/system-actions"
import * as billingActions from "../Billing/billing-actions"
import UpdatePlans from "./UpdatePlans"
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return {
system: state.system,
user: state.user
}
}
export default connect(
mapStateToProps,
systemActions
)(UpdatePlans)

View File

@@ -1,156 +0,0 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./system-constants";
import { push } from 'connected-react-router';
function beginUpdateSystem() {
return { type: types.UPDATE_SYSTEM };
}
function updateSystemSuccess(data) {
return { type: types.UPDATE_SUCCESS_SYSTEM, data };
}
function updateSystemError(data) {
return { type: types.UPDATE_ERROR_SYSTEM, data };
}
function beginFetchSystem() {
return { type: types.FETCH_SYSTEM };
}
function fetchSystemSuccess(data) {
return { type: types.FETCH_SUCCESS_SYSTEM,
data };
}
function fetchSystemError(data) {
return { type: types.FETCH_ERROR_SYSTEM,
data };
}
function beginUpdatePlan() {
return { type: types.UPDATE_PLAN };
}
function updatePlanSuccess(data) {
return { type: types.UPDATE_PLAN_SUCCESS,
data };
}
function updatePlanError(data) {
return { type: types.UPDATE_PLAN_ERROR,
data };
}
function makeUserRequest(method, data, api = "/login") {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
export function updatePlan(data) {
return dispatch => {
dispatch(beginUpdatePlan());
return makeUserRequest("post", {updatePlans:data}, account_server + "/api/plans/", {withCredentials: true})
.then(response => {
if (response.data.success) {
dispatch(updatePlanSuccess(response.data.systems));
} else {
dispatch(updatePlanError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
export function updateSystem(data) {
return dispatch => {
dispatch(beginUpdateSystem());
return makeUserRequest("post", data, admin_server + "/systems/" + data.shortName, {withCredentials: true})
.then(response => {
if (response.data.success) {
const user = response.data.user
dispatch(updateSystemSuccess(response.data.system));
dispatch(push("/system/"+data.shortName));
} else {
dispatch(updateProfileError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
function shouldFetchSystems(state, subreddit) {
if ((!state.system.items) || (state.system.items.length <1)) {
return true
} else if (state.system.isWaiting) {
return false
} else {
return false
}
}
export function fetchSystems() {
return (dispatch, getState) => {
if (shouldFetchSystems(getState())) {
dispatch(beginFetchSystem());
return axios
.get(admin_server + "/systems", {withCredentials: true})
.then(response => {
if (response.data.success) {
dispatch(fetchSystemSuccess(response.data.systems));
} else {
dispatch(fetchSystemError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}

View File

@@ -1,10 +0,0 @@
export const UPDATE_SYSTEM = "UPDATE_SYSTEM"
export const UPDATE_SUCCESS_SYSTEM = "UPDATE_SUCCESS_SYSTEM"
export const UPDATE_ERROR_SYSTEM = "UPDATE_ERROR_SYSTEM"
export const FETCH_SYSTEM = "FETCH_SYSTEM"
export const FETCH_SUCCESS_SYSTEM = "FETCH_SUCCESS_SYSTEM"
export const FETCH_ERROR_SYSTEM = "FETCH_ERROR_SYSTEM"
export const UPDATE_PLAN = "UPDATE_PLAN"
export const UPDATE_PLAN_SUCCESS = "UPDATE_PLAN_SUCCESS"
export const UPDATE_PLAN_ERROR = "UPDATE_PLAN_ERROR"

View File

@@ -1,78 +0,0 @@
import {
UPDATE_SYSTEM,
UPDATE_SUCCESS_SYSTEM,
UPDATE_ERROR_SYSTEM,
FETCH_SYSTEM,
FETCH_SUCCESS_SYSTEM,
FETCH_ERROR_SYSTEM,
UPDATE_PLAN,
UPDATE_PLAN_SUCCESS,
UPDATE_PLAN_ERROR
} from "./system-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateObjectInArray(array, shortName, updatedItem) {
return array.map( item => {
if(item.shortName !== shortName) {
// This isn't the item we care about - keep it as-is
return item;
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...updatedItem
};
});
}
const system = (
state = {
isWaiting: false,
items: []},
action
) => {
switch (action.type) {
case UPDATE_SYSTEM:
case FETCH_SYSTEM:
case UPDATE_PLAN:
return Object.assign({}, state, { isWaiting: true});
case UPDATE_PLAN_SUCCESS:
return Object.assign({}, state, {
isWaiting: false,
items: action.data
});
case UPDATE_SUCCESS_SYSTEM:
return Object.assign({}, state, {
items: updateObjectInArray(state.items, action.data.shortName, action.data)
});
case FETCH_SUCCESS_SYSTEM:
return Object.assign({}, state, {
isWaiting: false,
items: action.data
});
case UPDATE_PLAN_ERROR:
case UPDATE_ERROR_SYSTEM:
case FETCH_ERROR_SYSTEM:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default system;

View File

@@ -1,81 +0,0 @@
import React, { Component } from "react";
import {
Container,
Header,
Dimmer,
Loader,
Button,
Message,
Icon
} from "semantic-ui-react";
import { Link } from "react-router-dom";
class ConfirmEmail extends Component {
constructor(props) {
super(props);
}
state = {
loading: true,
confirmMessage: { success: false, message:""}
};
componentDidMount() {
const { match: { params } } = this.props;
this.props
.confirmEmail(params.userId, params.token)
.then(confirmMessage => {
if (confirmMessage) {
// report to the user is there was a problem during login
this.setState({
confirmMessage: confirmMessage, loading: false
});
}
});
}
render() {
var dimmerProps = { };
if (this.state.loading) {
dimmerProps["active"] = true;
}
return (
<Container text>
<Dimmer {...dimmerProps}>
<Loader indeterminate>Confirming Email Address</Loader>
</Dimmer>
{this.state.confirmMessage.success ? (
<Message icon>
<Icon name='check'/>
<Message.Content>
<Message.Header>Success</Message.Header>
<p>You have successfully confirmed you email address.</p>
<Link to="/">
<Button
size="large"
content="Continue to Login"
fluid
/></Link>
</Message.Content>
</Message>
) : (
<Message icon>
<Icon name='exclamation'/>
<Message.Content>
<Message.Header>Error</Message.Header>
<p>{this.state.confirmMessage.message}</p>
</Message.Content>
</Message>
)}
</Container>
);
}
}
export default ConfirmEmail;

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import ConfirmEmail from "./ConfirmEmail"
import * as userActions from "./user-actions"
const mapStateToProps = (state) => {
return {
user: state.user
}
}
export default connect(
mapStateToProps,
userActions
)(ConfirmEmail)

View File

@@ -1,196 +0,0 @@
import React, { PropTypes, Component } from "react";
import { Link } from "react-router-dom";
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Divider
} from "semantic-ui-react";
// ----------------------------------------------------
const dividerStyle = {
marginTop: "40px"
};
const forgotStyle = {
marginTop: "40px",
textAlign: "right"
};
// ----------------------------------------------------
class Login extends Component {
constructor(props) {
super(props);
this._onLoginSubmit = this._onLoginSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleRegisterClick = this.handleRegisterClick.bind(this);
}
state = {
loginMessage: "",
email: "",
password: ""
};
handleRegisterClick = () => {
this.props.changeUrl("/register");
};
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
_onLoginSubmit(event) {
event.preventDefault();
const { email, password } = this.state;
var success = {type: "path", nextPathname: "/" };
if (this.props.nextPathname) {
success.nextPathname = this.props.nextPathname;
}
if (this.props.nextLocation) {
success.type = "location";
switch(this.props.nextLocation) {
case "admin":
success.nextLocation = admin_server;
break;
case "frontend":
success.nextLocation = frontend_server;
break;
}
}
// Passed in via react-redux. Returns a promise.
this.props
.manualLogin(
{
// this function is passed in via react-redux
email,
password
},
success
) // holds the path to redirect to after login (if any)
.then(loginMessage => {
if (loginMessage) {
// report to the user is there was a problem during login
this.setState({
loginMessage
});
}
});
}
render() {
var loginMessage = "";
const { name, email } = this.state;
if (this.state.loginMessage) {
loginMessage = (
<Message icon>
<Icon name="lemon" />
<Message.Content>
<Message.Header>Problems...</Message.Header>
{this.state.loginMessage}
</Message.Content>
</Message>
);
}
return (
<Container>
<Header as="h1">{site_name}</Header>
<Grid stackable >
<Grid.Row>
<Grid.Column width={9}>
<Header size="large">Let's get this started!</Header>
<Header size="small">
{site_name} makes it easy to share recordings from{" "}
<a href="https://github.com/robotastic/trunk-recorder">
Trunk Recorder
</a>.</Header>
<p>
Trunk Recorder makes it easy to record the radio systems
used by your local police and fire department. Trunk Recorder
is sort of like an awesome version of those Scanners from
Radio Shack... 10-4!
</p>
<p>
This site is a work in progress. I plan on adding more
features over time, but right now it can:
</p>
<ul>
<li>3 day archive</li>
<li>Filter by talkgroup</li>
</ul>
<p>
All of this goodness is free! In the future, I might offer
paid accounts with additional features and a longer archive
period, but I will always try to have a free version.
</p>
<p>
{" "}
- Luke{" "}
<a
href="mailto:{admin_email}?Subject={site_name}"
target="_top"
>
admin@email.com
</a>{" "}
</p>
</Grid.Column>
<Grid.Column floated="right" width={6}>
<Header as="h3">Need an account?</Header>
<Button
size="large"
content="Register"
onClick={this.handleRegisterClick}
fluid
/>
<Divider style={dividerStyle} horizontal>Or</Divider>
<Header as="h3">Login</Header>
<Form onSubmit={this._onLoginSubmit}>
<Segment padded>
<Form.Field>
<Form.Input
icon="user"
iconPosition="left"
type="text"
name="email"
onChange={this.handleInputChange}
placeholder="E-mail address"
/>
</Form.Field>
<Form.Field>
<Form.Input
icon="lock"
iconPosition="left"
type="password"
name="password"
onChange={this.handleInputChange}
placeholder="Password"
/>
</Form.Field>
<Button type="submit" size="large" value="Login" fluid>
Login
</Button>
{loginMessage}
</Segment>
</Form>
<div style={forgotStyle} >
<Link to="/send-reset-password">Forgot Password</Link>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
</Container>
);
}
}
export default Login;

View File

@@ -1,36 +0,0 @@
import { connect } from "react-redux"
import { push } from 'react-router-redux'
import * as userActions from "./user-actions"
import Login from "./Login"
import queryString from '../query-string';
// Function passed in to `connect` to subscribe to Redux store updates.
// Any time it updates, mapStateToProps is called.
// The second argument "ownProps" contains props passed to the component
const mapStateToProps = (state, props) => {
let nextPathname = false;
try {nextPathname = props.location.state.nextPathname}
catch(err) {}
const uri = queryString.parse(props.location.search);
let nextLocation = uri['nextLocation'];
return {
user: state.user,
nextPathname, // this prop passed in by React Router
nextLocation
}
}
// Connects React component to the redux store
// It does not modify the component class passed to it
// Instead, it returns a new, connected component class, for you to use.
export default connect(
mapStateToProps,
userActions
)(Login)

View File

@@ -1,45 +0,0 @@
import React, { Component } from "react"
import UserForm from "./UserForm";
import {
Container,
Header
} from "semantic-ui-react";
class Profile extends Component {
constructor(props) {
super(props);
this.handleProfileSubmit = this.handleProfileSubmit.bind(this);
}
state = {
requestMessage: ""
}
handleProfileSubmit(user) {
user.userId = this.props.user.userId
this.props
.updateProfile(user)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
}
});
}
render() {
// The Key on UserForm get updated when the user is loaded. This forces the Component to be completely redrawn, otherwise the changes in the Prop are not copied to State.
return(
<Container text>
<Header as="h1">Update Profile</Header>
<UserForm onSubmit={this.handleProfileSubmit} user={this.props.user} isEditing={true} requestMessage={this.state.requestMessage} isWaiting={this.props.isWaiting} key={this.props.user.userId}/>
</Container>
)
}
}
export default Profile

View File

@@ -1,15 +0,0 @@
import { connect } from "react-redux"
import Profile from "./Profile"
import * as userActions from "./user-actions"
const mapStateToProps = (state) => {
return {
user: state.user,
isWaiting: state.user.isWaiting
}
}
export default connect(
mapStateToProps,
userActions
)(Profile)

View File

@@ -1,71 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon
} from "semantic-ui-react";
import UserForm from "./UserForm";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class Register extends Component {
constructor(props) {
super(props);
this._onRegisterSubmit = this._onRegisterSubmit.bind(this);
}
state = {
requestMessage: ""
}
_onRegisterSubmit(user) {
this.props
.manualRegister(user)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
}
});
}
render() {
return (
<div>
<Container text>
<Message icon>
<Icon name='trophy'/>
<Message.Content>
<Message.Header>Welcome!</Message.Header>
<p>Creating an account on OpenMHz is the first step in sharing radio system recordings using Trunk Recorder.</p>
<p>After creating an account, head over <a href="https://github.com/robotastic/trunk-recorder">here</a> to learn more about Trunk Recorder</p>
<p> - Luke</p>
</Message.Content>
</Message>
</Container>
<Container text>
<Header style={{ paddingTop: '20px' }} as="h1">Register</Header>
<UserForm onSubmit={this._onRegisterSubmit} requestMessage={this.state.requestMessage} isWaiting={this.props.isWaiting}/>
</Container>
</div>
);
}
}
export default Register;

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import * as userActions from "./user-actions"
import Register from "./Register"
const mapStateToProps = (state) => {
return {
isWaiting: state.user.isWaiting
}
}
export default connect(
mapStateToProps,
userActions
)(Register)

View File

@@ -1,113 +0,0 @@
import React, { Component } from "react";
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon
} from "semantic-ui-react";
class ResetPassword extends Component {
constructor(props) {
super(props);
this.handleResetPassword = this.handleResetPassword.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
state = {
checkInputMessages: [],
password: "",
confirmPassword: "",
passwordError: false,
confirmPasswordError: false,
sentReset: false
};
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleResetPassword(event) {
let error = false;
var checkInputMessages = [];
if (this.state.password === "") {
this.setState({ passwordError: true });
checkInputMessages.push("Password is required");
error = true;
} if (this.state.password.length < 7) {
this.setState({ passwordError: true });
checkInputMessages.push("Password must be 7 charecters or more");
error = true;
}else {
this.setState({ passwordError: false });
}
if (this.state.password !== this.state.confirmPassword) {
this.setState({ confirmPasswordError: true });
checkInputMessages.push("The passwords did not match");
error = true;
} else {
this.setState({ confirmPasswordError: false });
}
const { match: { params } } = this.props;
this.setState({ checkInputMessages: checkInputMessages });
if (!error) {
this.props.resetPassword(params.userId, params.token, this.state.password).then(resetSent => {
this.setState({resetSent: true});
});
}
}
render() {
return (
<div>
<Container text>
<Segment>
<Form
className="raised padding segment"
onSubmit={this.handleSubmit}
>
<Form.Group widths="equal">
<Form.Field>
<Form.Input
type="text"
name="password"
type="password"
onChange={this.handleInputChange}
error={this.state.passwordError}
label="Password"
placeholder="Password..."
/>
</Form.Field>
<Form.Field>
<Form.Input
type="text"
name="confirmPassword"
type="password"
onChange={this.handleInputChange}
error={this.state.confirmPasswordError}
label="Confirm Password"
placeholder="Confirm Password..."
/>
</Form.Field>
</Form.Group>
<Button
size="large"
content="Reset Password"
onClick={this.handleResetPassword}
fluid
/>
</Form>
</Segment>
</Container>
</div>
);
}
}
export default ResetPassword;

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import ResetPassword from "./ResetPassword"
import * as userActions from "./user-actions"
const mapStateToProps = (state) => {
return {
user: state.user
}
}
export default connect(
mapStateToProps,
userActions
)(ResetPassword)

View File

@@ -1,115 +0,0 @@
import React, { Component } from "react";
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon
} from "semantic-ui-react";
import { Link } from "react-router-dom";
const returnStyle = {
marginTop: "30px"
};
class SendResetPassword extends Component {
constructor(props) {
super(props);
this.handleSendResetPassword = this.handleSendResetPassword.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
state = {
checkInputMessages: "",
email: "",
emailError: false,
sentReset: false
};
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleSendResetPassword(event) {
if (this.state.email === "") {
this.setState({ emailError: true, checkInputMessages: "Email is required"});
} else {
this.setState({ emailError: false });
this.props.sendResetPassword({email: this.state.email}).then(resetMessage => {
if (resetMessage) {
// report to the user is there was a problem during login
this.setState({resetSent: false, checkInputMessages: resetMessage, emailError: true});
} else {
this.setState({resetSent: true});
}
});
}
}
render() {
var resetMessage = "";
if (this.state.checkInputMessages.length) {
resetMessage = (
<Message compact warning icon>
<Icon name="lightning" />
<Message.Content>
<Message.Header>Problems...</Message.Header>
{this.state.checkInputMessages}
</Message.Content>
</Message>
);
}
return (
<Grid verticalAlign='middle' centered>
<Grid.Row>
<Grid.Column width="6">
{ !this.state.resetSent ? (
<div>
<h2>Forgot your password?</h2>
<Form className="raised padding segment" onSubmit={this.handleSubmit}>
Enter the email address for your account.
<Form.Field>
<Form.Input
type="text"
name="email"
onChange={this.handleInputChange}
error={this.state.emailError}
label="Email"
placeholder="Email..."
/>
</Form.Field>
<Button
size="large"
content="Reset Password"
onClick={this.handleSendResetPassword}
fluid
/>
</Form>
{resetMessage}
</div>
)
:( <Segment raised padded='very'>
<h2>Reset Link Sent </h2>
<div>An email with a link to reset your password was sent to: {this.state.email}</div>
<Link to="/" style={returnStyle}><Icon name="left arrow"/>Back to OpenMHz</Link>
</Segment>
)}
</Grid.Column>
</Grid.Row>
</Grid>
);
}
}
export default SendResetPassword;

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import SendResetPassword from "./SendResetPassword"
import * as userActions from "./user-actions"
const mapStateToProps = (state) => {
return {
user: state.user
}
}
export default connect(
mapStateToProps,
userActions
)(SendResetPassword)

View File

@@ -1,36 +0,0 @@
import React, { Component } from "react"
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon
} from "semantic-ui-react";
class SentConfirmEmail extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
<Container text>
<Message icon>
<Icon name='envelope outline'/>
<Message.Content>
<Message.Header>Confirmation email sent!</Message.Header>
<p>We sent an email to the address you gave us: {this.props.user.email}<br/>
Please click on the link in the email to verify the email address.</p>
</Message.Content>
</Message>
</Container>
</div>
)
}
}
export default SentConfirmEmail

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import SentConfirmEmail from "./SentConfirmEmail"
import * as userActions from "./user-actions"
const mapStateToProps = (state) => {
return {
user: state.user
}
}
export default connect(
mapStateToProps,
userActions
)(SentConfirmEmail)

View File

@@ -1,83 +0,0 @@
import React, { Component } from "react";
import { Segment,
TextArea,
Container,
Button } from "semantic-ui-react";
class Terms extends Component {
constructor(props) {
super(props);
this.handleAccept = this.handleAccept.bind(this);
this.handleDecline = this.handleDecline.bind(this);
}
handleAccept() {
this.props
.acceptTerms(this.props.user.userId)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
}
});
}
handleDecline() {
}
render() {
return (
<div>
<Container text>
<Segment attached>
The agreement is useful to The Guardian to set the rules and guidelines for visitors, users and customers:
Registration: inform users that they must agree to a set of rules when they register on your website or mobile app
Disable or terminate accounts: inform users that their accounts can be disabled or terminated if abuses happen on your website or mobile app, e.g. spamming, swearing etc.
Owner of content: notify users that you, the company, is the owner of the content appearing on your website or mobile app – except in cases where other users can post content (upload, share, create etc.) where the users are the owners of such content
Changes to the agreement: users should be informed about upcoming changes to the agreement before the changes are applied.
DMCA: The Guardian is also informing owners of copyright content that they can submit a DMCA notice to the website if they found infringing content in their DMCA clause.
And more
These are only a few examples of what kind of clauses and disclosures you can add in this kind of agreement.
What is a Terms of Use
While a Terms of Use is recommended to have, it’s not mandatory by law as the Privacy Policy agreement is required.
This agreement serves a single purpose: to inform users of rules that they must agree to. A Privacy Policy must inform users about your privacy practices (what data you collect, why, how, etc.)
The definition of a Terms of Use agreement is simple:
Terms of service (also known as terms of use and terms and conditions, commonly abbreviated as ToS or TOS and TOU) are rules which one must agree to abide by in order to use a service.
Wikipedia
Because this agreement simply acts as a contract between you, the company, and the users using or accessing your website or mobile app, the agreement can be named as you’d like:
Terms of Use (ToU)
Terms of Service (ToS)
Terms and Conditions (T&C)
User Agreement
Conditions of Use
And so on
The “Conditions of Use” of Amazon could be named “Terms of Use”:
</Segment>
<Button.Group attached="bottom">
<Button
onClick={this.handleDecline}
>Decline</Button>
<Button.Or />
<Button positive
onClick={this.handleAccept}
>Accept</Button>
</Button.Group>
</Container>
</div>
);
}
}
export default Terms;

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import * as userActions from "./user-actions"
import Terms from "./Terms"
function mapStateToProps(state) {
return {
user: state.user
}
}
export default connect(
mapStateToProps,
userActions
)(Terms)

View File

@@ -1,292 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Form,
Grid,
Dimmer,
Loader,
Segment,
Input,
Button,
Message,
Icon
} from "semantic-ui-react";
// ----------------------------------------------------
const checkInputMessagestyle = {
color: "red"
};
// ----------------------------------------------------
class UserForm extends Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.checkInputs = this.checkInputs.bind(this);
this.render = this.render.bind(this)
this.state = {
checkInputMessages: [],
registerMessages: "",
firstName: "",
lastName: "",
screenName: "",
email: "",
location: "",
password: "",
confirmPassword: "",
firstNameError: false,
lastNameError: false,
screenNameError: false,
emailError: false,
locationError: false,
passwordError: false,
confirmPasswordError: false,
isEditing: this.props.isEditing
};
if (this.props.isEditing) {
this.state.firstName = this.props.user.firstName;
this.state.lastName = this.props.user.lastName;
this.state.screenName = this.props.user.screenName;
this.state.email = this.props.user.email;
this.state.location = this.props.user.location;
}
}
checkInputs() {
let error = false;
var checkInputMessages= [];
if (this.state.firstName === "") {
this.setState({ firstNameError: true });
checkInputMessages.push("First Name is required");
error = true;
} else {
this.setState({ firstNameError: false });
}
if (this.state.lastName === "") {
this.setState({ lastNameError: true });
checkInputMessages.push("Last Name is required");
error = true;
} else {
this.setState({ lastNameError: false });
}
if (this.state.email === "") {
this.setState({ emailError: true });
checkInputMessages.push("Email is required");
error = true;
} else {
this.setState({ emailError: false });
}
if (this.state.location === "") {
this.setState({ locationError: true });
checkInputMessages.push("Location is required");
error = true;
} else {
this.setState({ locationError: false });
}
if (this.state.screenName === "") {
this.setState({ screenNameError: true });
checkInputMessages.push("Screen Name is required");
error = true;
} else {
this.setState({ screenNameError: false });
}
if (this.props.isEditing) {
this.setState({ checkInputMessages: checkInputMessages });
return error;
}
if (this.state.password === "") {
this.setState({ passwordError: true });
checkInputMessages.push("Password is required");
error = true;
} if (this.state.password.length < 7) {
this.setState({ passwordError: true });
checkInputMessages.push("Password must be 7 charecters or more");
error = true;
}else {
this.setState({ passwordError: false });
}
if (this.state.password !== this.state.confirmPassword) {
this.setState({ confirmPasswordError: true });
checkInputMessages.push("The passwords did not match");
error = true;
} else {
this.setState({ confirmPasswordError: false });
}
this.setState({ checkInputMessages: checkInputMessages });
return error;
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleSubmit(event) {
event.preventDefault();
let inputError = this.checkInputs();
if (!inputError) {
const user = (({ firstName, lastName, screenName, location, email, password }) => ({ firstName, lastName, screenName, location, email, password }))(this.state);
this.props.onSubmit(user);
}
}
render() {
var registerMessage = "";
var floatStyle = {
overflow: 'auto'
}
var dimmerProps = { };
if (this.props.isWaiting) {
dimmerProps["active"] = true;
}
const { name, email } = this.state;
const { isEditing} = this.props;
if (this.state.checkInputMessages.length || this.props.requestMessage.length) {
var counter = 0;
registerMessage = (
<Message compact warning icon>
<Icon name="lightning" />
<Message.Content>
<Message.Header>Problems...</Message.Header>
<ul>
{this.state.checkInputMessages.map(function(listValue) {
return <li key={`ListItem_${counter++}`}>{listValue}</li>;
})}
{ this.props.requestMessage.length>0 &&
<li key="req-mess">{this.props.requestMessage}</li>
}
</ul>
</Message.Content>
</Message>
);
}
return (
<div>
<Form
className="raised padding segment"
onSubmit={this.handleSubmit}
>
<Dimmer {...dimmerProps}>
<Loader indeterminate>Updating Profile</Loader>
</Dimmer>
<Form.Group widths="equal">
<Form.Field>
<Form.Input
type="text"
name="firstName"
onChange={this.handleInputChange}
error={this.state.firstNameError}
defaultValue={this.state.firstName}
label="First name"
placeholder="First Name..."
/>
</Form.Field>
<Form.Field>
<Form.Input
type="text"
name="lastName"
onChange={this.handleInputChange}
error={this.state.lastNameError}
defaultValue={this.state.lastName}
label="Last name"
placeholder="Last Name..."
/>
</Form.Field>
</Form.Group>
<Form.Group widths="equal">
<Form.Field>
<Form.Input
type="text"
name="email"
onChange={this.handleInputChange}
error={this.state.emailError}
defaultValue={this.state.email}
disabled={isEditing}
label="Email"
placeholder="Email..."
/>
</Form.Field>
<Form.Field>
<Form.Input
type="text"
name="screenName"
onChange={this.handleInputChange}
error={this.state.screenNameError}
defaultValue={this.state.screenName}
label="Screen Name"
placeholder="Screen Name..."
/>
<p style={{ fontStyle: "italic" }}>
Shown on OpenMHz for each of your systems
</p>
</Form.Field>
</Form.Group>
<Form.Group widths="equal">
<Form.Field>
<Form.Input
type="text"
name="location"
onChange={this.handleInputChange}
error={this.state.locationError}
defaultValue={this.state.location}
label="General Location"
placeholder="City, State..."
/>
</Form.Field>
</Form.Group>
{isEditing!=true && (
<Form.Group widths="equal">
<Form.Field>
<Form.Input
type="text"
name="password"
type="password"
onChange={this.handleInputChange}
error={this.state.passwordError}
disabled={isEditing}
label="Password"
placeholder="Password..."
/>
</Form.Field>
<Form.Field>
<Form.Input
type="text"
name="confirmPassword"
type="password"
onChange={this.handleInputChange}
error={this.state.confirmPasswordError}
disabled={isEditing}
label="Confirm Password"
placeholder="Confirm Password..."
/>
</Form.Field>
</Form.Group>
)}
<div style={floatStyle}>
<Button type="submit" size="large" floated="right" value="Login" color='blue'>
{isEditing!=true? 'Register' : 'Update' }
</Button>
</div>
</Form>
{registerMessage}
</div>
);
}
}
export default UserForm;

View File

@@ -1,53 +0,0 @@
import React, { Component } from "react"
import { Link } from 'react-router-dom'
import {
Container,
Header,
Button,
Message,
Icon
} from "semantic-ui-react";
class WaitConfirmEmail extends Component {
constructor(props) {
super(props);
this.handleSendConfirmEmail = this.handleSendConfirmEmail.bind(this);
}
handleSendConfirmEmail(event) {
this.props.sendConfirmEmail(this.props.user.userId).then(loginMessage => {
if (loginMessage) {
// report to the user is there was a problem during login
this.setState({
loginMessage
});
}
});
};
render() {
return(
<Container text>
<Message icon>
<Icon name='envelope outline'/>
<Message.Content>
<Message.Header>Confirm your email address</Message.Header>
<p>We sent an email to the address you gave us: {this.props.user.email}<br/>
Please click on the link in the email to verify the email address.</p>
<Header as='h3'>Didn't get an email?</Header>
<p>So, you have waited a bit and haven't gotten anything?</p>
<p>...and you checked your spam folder?</p>
<p>Well, click below.</p>
<Button
size="large"
content="Resend Email Confirmation"
onClick={this.handleSendConfirmEmail}
/>
</Message.Content>
</Message>
</Container>
)
}
}
export default WaitConfirmEmail

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import WaitConfirmEmail from "./WaitConfirmEmail"
import * as userActions from "./user-actions"
const mapStateToProps = (state) => {
return {
user: state.user
}
}
export default connect(
mapStateToProps,
userActions
)(WaitConfirmEmail)

View File

@@ -1,389 +0,0 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./user-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginLogin() {
return { type: types.MANUAL_LOGIN_USER };
}
function loginSuccess(data) {
return {
type: types.LOGIN_SUCCESS_USER,
data
};
}
function loginEmailError(data) {
return {
type: types.LOGIN_EMAIL_ERROR_USER,
data
};
}
function loginError() {
return { type: types.LOGIN_ERROR_USER };
}
// "update profile" action creators
function beginUpdateProfile() {
return { type: types.UPDATE_PROFILE_USER };
}
function updateProfileSuccess(data) {
return { type: types.UPDATE_PROFILE_SUCCESS_USER, data };
}
function updateProfileError() {
return { type: types.UPDATE_PROFILE_ERROR_USER };
}
// "Reset Password" action creators
function beginResetPassword() {
return { type: types.RESET_PASSWORD_USER };
}
function resetPasswordSuccess() {
return { type: types.RESET_PASSWORD_SUCCESS_USER };
}
function resetPasswordError() {
return { type: types.RESET_PASSWORD_ERROR_USER };
}
// "Send Reset Password" action creators
function beginSendResetPassword() {
return { type: types.SEND_RESET_PASSWORD_USER };
}
function sendResetPasswordSuccess() {
return { type: types.SEND_RESET_PASSWORD_SUCCESS_USER };
}
function sendResetPasswordError() {
return { type: types.SEND_RESET_PASSWORD_ERROR_USER };
}
// "Confirm" action creators
function beginConfirm() {
return { type: types.CONFIRM_USER };
}
function confirmSuccess() {
return { type: types.CONFIRM_SUCCESS_USER };
}
function confirmError() {
return { type: types.CONFIRM_ERROR_USER };
}
// "Send Confirm" action creators
function beginSendConfirm() {
return { type: types.SEND_CONFIRM_USER };
}
function sendConfirmSuccess() {
return { type: types.SEND_CONFIRM_SUCCESS_USER };
}
function sendConfirmError() {
return { type: types.SEND_CONFIRM_ERROR_USER };
}
// "Log Out" action creators
function beginLogout() {
return { type: types.LOGOUT_USER };
}
function logoutSuccess() {
return { type: types.LOGOUT_SUCCESS_USER };
}
function logoutError() {
return { type: types.LOGOUT_ERROR_USER };
}
// "Register" action creators
function beginRegister() {
return { type: types.REGISTER_USER };
}
function registerSuccess(data) {
return {
type: types.REGISTER_SUCCESS_USER,
data
};
}
function registerError() {
return { type: types.REGISTER_ERROR_USER };
}
// "Terms" action creators
function beginAgreeTerms() {
return { type: types.AGREE_TERMS_USER };
}
function agreeTermsSuccess() {
return { type: types.AGREE_TERMS_SUCCESS_USER };
}
function agreeTermsError() {
return { type: types.AGREE_TERMS_ERROR_USER };
}
function makeUserRequest(method, data, api = "/login") {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
export function manualLogin(
data,
success // path to redirect to upon successful log in
) {
return dispatch => {
dispatch(beginLogin());
return makeUserRequest("post", data, "/login", {withCredentials: true})
.then(response => {
if (response.data.success) {
const user = response.data.user
dispatch(loginSuccess(user));
// use browserHistory singleton to control navigation. Will generate a
// state change for time-traveling as we are using the react-router-redux package
//browserHistory.push(successPath)
if (success && (success.type=="path")) {
dispatch(push(success.nextPathname));
} else if (success && (success.type=="location")) {
window.location=success.nextLocation;
} else {
dispatch(push("/"));
}
} else {
let loginMessage = response.data.message;
console.log("loginMessage");
if (loginMessage == "unconfirmed email") {
data.userId = response.data.userId;
dispatch(loginEmailError(data));
dispatch(push("/wait-confirm-email"));
} else {
dispatch(loginError());
}
return loginMessage;
}
})
.catch(function(response) {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
export function isAuthenticated() {
return dispatch => {
return axios
.get("/authenticated")
.then(response => {
if (response.data.success) {
dispatch(loginSuccess());
return true;
} else {
dispatch(loginError());
return false;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened during logout that triggered an Error
console.log("Error", response.message);
}
});
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
export function manualLogout() {
return dispatch => {
dispatch(beginLogout());
return axios
.get("/logout")
.then(response => {
if (response.data.success) {
dispatch(logoutSuccess());
// use browserHistory singleton to control navigation. Will generate a
// state change for time-traveling as we are using the react-router-redux package
//browserHistory.push("/") // logout to home page
dispatch(push("/"));
} else {
dispatch(logoutError());
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened during logout that triggered an Error
console.log("Error", response.message);
}
});
};
}
export function confirmEmail(userId, token) {
return dispatch => {
dispatch(beginConfirm());
return makeUserRequest("post", {}, "/users/" + userId + "/confirm/" + token).then(
response => {
if (response.data.success) {
dispatch(confirmSuccess());
let requestMessage = {success: true, message: response.data.message};
return requestMessage;
} else {
dispatch(confirmError());
let requestMessage = {success: false, message: response.data.message};
return requestMessage;
}
}
);
};
}
export function resetPassword(userId, token,password) {
return dispatch => {
dispatch(beginResetPassword());
return makeUserRequest("post", {password: password}, "/users/" + userId + "/reset-password/" + token).then(
response => {
if (response.data.success) {
dispatch(resetPasswordSuccess());
dispatch(push("/login"));
} else {
dispatch(resetPasswordError());
let requestMessage = response.data.message;
return requestMessage;
}
}
);
};
}
export function sendResetPassword(data) {
return dispatch => {
dispatch(beginSendResetPassword());
return makeUserRequest("post", data, "/api/send-reset-password").then(
response => {
if (response.data.success) {
dispatch(sendResetPasswordSuccess());
} else {
dispatch(sendResetPasswordError());
return response.data.message;
}
}
);
};
}
export function sendConfirmEmail(data) {
return dispatch => {
dispatch(beginSendConfirm());
return makeUserRequest("post", data, "/users/" + data + "/send-confirm").then(
response => {
if (response.data.success) {
dispatch(sendConfirmSuccess());
dispatch(push("/sent-confirm-email"));
} else {
dispatch(sendConfirmError());
let requestMessage = response.data.Message;
return requestMessage;
}
}
);
};
}
export function acceptTerms(data) {
return dispatch => {
dispatch(beginAgreeTerms());
return makeUserRequest("post", data, "/users/" + data + "/terms").then(
response => {
if (response.data.success) {
dispatch(agreeTermsSuccess());
} else {
dispatch(agreeTermsError());
let requestMessage = response.data.Message;
return requestMessage;
}
}
);
};
}
export function updateProfile(data) {
return dispatch => {
dispatch(beginUpdateProfile());
return makeUserRequest("post", data, "/users/" + data.userId)
.then(response => {
if (response.data.success) {
const user = response.data.user
dispatch(updateProfileSuccess(user));
dispatch(push("/"));
} else {
dispatch(updateProfileError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
export function manualRegister(data) {
return dispatch => {
dispatch(beginRegister());
return makeUserRequest("post", data, "/register")
.then(response => {
if (response.data.success) {
data.userId = response.data.userId;
dispatch(registerSuccess(data));
dispatch(push("/sent-confirm-email"));
} else {
dispatch(registerError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}

View File

@@ -1,31 +0,0 @@
export const MANUAL_LOGIN_USER = "MANUAL_LOGIN_USER"
export const LOGIN_SUCCESS_USER = "LOGIN_SUCCESS_USER"
export const LOGIN_EMAIL_ERROR_USER = "LOGIN_EMAIL_ERROR_USER"
export const LOGIN_ERROR_USER = "LOGIN_ERROR_USER"
export const CONFIRM_USER = "CONFIRM_USER"
export const CONFIRM_ERROR_USER = "CONFIRM_ERROR_USER"
export const CONFIRM_SUCCESS_USER = "CONFIRM_SUCCESS_USER"
export const RESET_PASSWORD_USER = "RESET_PASSWORD_USER"
export const RESET_PASSWORD_ERROR_USER = "RESET_PASSWORD_ERROR_USER"
export const RESET_PASSWORD_SUCCESS_USER = "RESET_PASSWORD_SUCCESS_USER"
export const SEND_RESET_PASSWORD_USER = "SEND_RESET_PASSWORD_USER"
export const SEND_RESET_PASSWORD_ERROR_USER = "SEND_RESET_PASSWORD_ERROR_USER"
export const SEND_RESET_PASSWORD_SUCCESS_USER = "SEND_RESET_PASSWORD_SUCCESS_USER"
export const SEND_CONFIRM_USER = "SEND_CONFIRM_USER"
export const SEND_CONFIRM_ERROR_USER = "SEND_CONFIRM_ERROR_USER"
export const SEND_CONFIRM_SUCCESS_USER = "SEND_CONFIRM_SUCCESS_USER"
export const SIGNUP_USER = "SIGNUP_USER"
export const SIGNUP_SUCCESS_USER = "SIGNUP_SUCCESS_USER"
export const SIGNUP_ERROR_USER = "SIGNUP_ERROR_USER"
export const LOGOUT_USER = "LOGOUT_USER"
export const LOGOUT_SUCCESS_USER = "LOGOUT_SUCCESS_USER"
export const LOGOUT_ERROR_USER = "LOGOUT_ERROR_USER"
export const UPDATE_PROFILE_USER = "UPDATE_PROFILE_USER"
export const UPDATE_PROFILE_SUCCESS_USER = "UPDATE_PROFILE_SUCCESS_USER"
export const UPDATE_PROFILE_ERROR_USER = "UPDATE_PROFILE_ERROR_USER"
export const REGISTER_USER = "REGISTER_USER"
export const REGISTER_SUCCESS_USER = "REGISTER_SUCCESS_USER"
export const REGISTER_ERROR_USER = "REGISTER_ERROR_USER"
export const AGREE_TERMS_USER = "AGREE_TERMS_USER"
export const AGREE_TERMS_SUCCESS_USER = "AGREE_TERMS_SUCCESS_USER"
export const AGREE_TERMS_ERROR_USER = "AGREE_TERMS_ERROR_USER"

View File

@@ -1,139 +0,0 @@
import {
MANUAL_LOGIN_USER,
LOGIN_SUCCESS_USER,
LOGIN_ERROR_USER,
LOGIN_EMAIL_ERROR_USER,
SEND_RESET_PASSWORD_USER,
SEND_RESET_PASSWORD_ERROR_USER,
SEND_RESET_PASSWORD_SUCCESS_USER,
RESET_PASSWORD_USER,
RESET_PASSWORD_ERROR_USER,
RESET_PASSWORD_SUCCESS_USER,
CONFIRM_USER,
CONFIRM_ERROR_USER,
CONFIRM_SUCCESS_USER,
UPDATE_PROFILE_USER,
UPDATE_PROFILE_ERROR_USER,
UPDATE_PROFILE_SUCCESS_USER,
SEND_CONFIRM_USER,
SEND_CONFIRM_ERROR_USER,
SEND_CONFIRM_SUCCESS_USER,
LOGOUT_USER,
LOGOUT_SUCCESS_USER,
LOGOUT_ERROR_USER,
REGISTER_USER,
REGISTER_SUCCESS_USER,
REGISTER_ERROR_USER
} from "./user-constants";
const user = (
state = {
isWaiting: false,
authenticated: false,
userId: ""
},
action
) => {
switch (action.type) {
case MANUAL_LOGIN_USER:
return Object.assign({}, state, { isWaiting: true });
case LOGIN_SUCCESS_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: true,
...action.data
});
case LOGIN_EMAIL_ERROR_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: false,
userId: action.data.userId
});
case LOGIN_ERROR_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: false
});
case UPDATE_PROFILE_USER:
return Object.assign({}, state, {
isWaiting: true
});
case UPDATE_PROFILE_SUCCESS_USER:
return Object.assign({}, state, {
isWaiting: false,
...action.data
});
case UPDATE_PROFILE_ERROR_USER:
return Object.assign({}, state, {
isWaiting: false
});
case CONFIRM_USER:
return Object.assign({}, state, {
isWaiting: true,
authenticated: false
});
case CONFIRM_SUCCESS_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: false
});
case CONFIRM_ERROR_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: false
});
case RESET_PASSWORD_USER:
case SEND_RESET_PASSWORD_USER:
case SEND_CONFIRM_USER:
return Object.assign({}, state, {
isWaiting: true,
authenticated: false
});
case RESET_PASSWORD_ERROR_USER:
case RESET_PASSWORD_SUCCESS_USER:
case SEND_RESET_PASSWORD_ERROR_USER:
case SEND_RESET_PASSWORD_SUCCESS_USER:
case SEND_CONFIRM_ERROR_USER:
case SEND_CONFIRM_SUCCESS_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: false
});
case LOGOUT_USER:
return Object.assign({}, state, { isWaiting: true });
case LOGOUT_SUCCESS_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: false,
email: "",
userId: ""
});
case LOGOUT_ERROR_USER:
return Object.assign({}, state, {
isWaiting: false,
authenticated: true
});
case REGISTER_USER:
return Object.assign({}, state, { isWaiting: true });
case REGISTER_SUCCESS_USER:
return Object.assign({}, state, { isWaiting: false,
userId: action.data.userId });
case REGISTER_ERROR_USER:
return Object.assign({}, state, { isWaiting: false });
default:
return state;
}
};
export default user;

View File

@@ -1,95 +0,0 @@
import React from "react"
import { render } from "react-dom"
import { Provider } from "react-redux"
import { Switch, Route} from 'react-router-dom'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from "redux-thunk"
import {createLogger} from "redux-logger"
//import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux'
import { routerMiddleware } from 'connected-react-router'
import { ConnectedRouter } from 'connected-react-router'
import isNode from "detect-node"
//import rootReducer from "./reducers"
import createRootReducer from './reducers'
import { createHashHistory } from 'history'
import createHistory from 'history/createBrowserHistory'
import App from "./components/container/App"
import LoginContainer from "./User/LoginContainer"
import RegisterContainer from "./User/RegisterContainer"
import ProfileContainer from "./User/ProfileContainer"
import BillingContainer from "./Billing/BillingContainer"
import Default from "./components/pure/Default"
import NavigationContainer from "./components/container/NavigationContainer"
import ConfirmEmailContainer from "./User/ConfirmEmailContainer"
import WaitConfirmEmailContainer from "./User/WaitConfirmEmailContainer"
import SentConfirmEmailContainer from "./User/SentConfirmEmailContainer"
import SendResetPasswordContainer from "./User/SendResetPasswordContainer"
import ResetPasswordContainer from "./User/ResetPasswordContainer"
import Restricted from "./components/container/Restricted"
import TermsContainer from "./User/TermsContainer"
import UpdatePlans from "./Plan/UpdatePlansContainer"
// Create a history of your choosing (we're using a browser history in this case)
const history = createHistory()
// Build the middleware for intercepting and dispatching navigation actions
const historyMiddleware = routerMiddleware(history)
let middleware = [
thunk,
historyMiddleware
]
// only add Redux-Logger on the client-side because it causes problems server-side.
if (!isNode) {
middleware.push(createLogger())
}
// Add the reducer to your store on the `router` key
// Also apply our middleware for navigating
const store = createStore(
createRootReducer(history),
applyMiddleware(thunk,
historyMiddleware, createLogger())
)
/*
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
<Route path="/update-system/:shortName" component={Restricted(EditSystemContainer, store)} />
<Route path="/create-system" component={Restricted(CreateSystemContainer, store)} />
<Route path="/list-systems" component={Restricted(ListSystemsContainer, store)} />
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
<Route path="/update-system/:shortName" component={EditSystemContainer} />
<Route path="/create-system" component={CreateSystemContainer} />
<Route path="/list-systems" component={ListSystemsContainer} />
*/
render(
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<App>
<Switch>
<Route exact path="/" component={Restricted(ProfileContainer, store)} />
<Route path="/login" component={LoginContainer} />
<Route path="/register" component={RegisterContainer} />
<Route path="/wait-confirm-email" component={WaitConfirmEmailContainer} />
<Route path="/sent-confirm-email" component={SentConfirmEmailContainer} />
<Route path="/send-reset-password" component={SendResetPasswordContainer} />
<Route path="/reset-password/:userId/:token" component={ResetPasswordContainer} />
<Route path="/confirm-email/:userId/:token" component={ConfirmEmailContainer} />
<Route path="/terms" component={Restricted(TermsContainer, store)} />
<Route path="/billing" component={Restricted(BillingContainer, store)} />
<Route path="/profile" component={Restricted(ProfileContainer, store)} />
<Route path="/update-plans" component={Restricted(UpdatePlans, store)} />
</Switch>
</App>
</div>
</ConnectedRouter>
</Provider>
, document.getElementById("app"))

View File

@@ -1,19 +0,0 @@
import React, { Component } from "react"
import NavigationContainer from "./NavigationContainer"
class App extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
<NavigationContainer />
{this.props.children}
</div>
)
}
}
export default App

View File

@@ -1,21 +0,0 @@
import { connect } from "react-redux"
import * as userActions from "../../User/user-actions"
import Navigation from "../pure/Navigation"
// Function passed in to `connect` to subscribe to Redux store updates.
// Any time it updates, mapStateToProps is called.
const mapStateToProps = (state) => {
return {
user: state.user
}
}
// Connects React component to the redux store
// It does not modify the component class passed to it
// Instead, it returns a new, connected component class, for you to use.
export default connect(
mapStateToProps,
userActions
)(Navigation)

View File

@@ -1,55 +0,0 @@
// in src/restricted.js
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import * as userActions from "../../User/user-actions"
import * as types from "../../User/user-constants";
import axios from "axios";
/**
* Higher-order component (HOC) to wrap restricted pages
*/
export default (BaseComponent, store) => {
class Restricted extends Component {
componentWillMount() {
this.checkAuthentication(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.location !== this.props.location) {
this.checkAuthentication(nextProps);
}
}
checkAuthentication(params) {
const { history } = params;
const authenticated = store.getState().user.authenticated;
if (!authenticated) {
axios
.get("/authenticated")
.then(response => {
if (response.data.success) {
const user = response.data.user
store.dispatch({
type: types.LOGIN_SUCCESS_USER,
data: user
});
} else {
history.replace({ pathname: '/login',
state: { nextPathname: params.location.pathname } });
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened during logout that triggered an Error
history.replace({ pathname: '/login',
state: { nextPathname: params.location.pathname } });
console.log("Error", response.message);
}
});
}
}
render() {
return <BaseComponent {...this.props} />;
}
}
return withRouter(Restricted);
}

View File

@@ -1,31 +0,0 @@
import React, { Component } from "react"
class Default extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
<h2>React-Passport-Redux-Example</h2>
<p>Demonstration of PassportJS authentication in React w/ Redux. Use the links above to register a new user and login/logout. You will only be able to view the MyProfile page when you are logged in. Logging in will redirect you to MyProfile if you had clicked that link beforehand.</p>
<p>I'll be doing my best to document code with useful comments so look for more detail there. Note: you should be able to implement the authentication functionality using less packages. I was working on another project before deciding to make this example repo so some bloat may have travelled over from that.</p>
<p>In full, this repo demonstrates:</p>
<ul>
<li><strong>React</strong> for the view layer </li>
<li><strong>PassportJS</strong> for authentication (using a Local Strategy)</li>
<li><strong>Redux</strong> to handle our state</li>
<li><strong>React Router</strong> for routing client-side</li>
<li><strong>Express</strong> for handling server requests (REST and page requests)</li>
<li><strong>MongoDB</strong> for our database, with <strong>Mongoose</strong> handling our schema</li>
<li><strong>Webpack</strong> to bundle our client-side code</li>
<li>Hot reloading using <strong>webpack-dev-middleware</strong> and <strong>webpack-hot-middleware</strong></li>
</ul>
<p>Shoutout to GitHub member <b>choonkending</b> whose <a href="https://github.com/choonkending/react-webpack-node" target="_blank">repo</a> was a big help. Pull requests and comments / issue reports are most welcome!</p>
</div>
)
}
}
export default Default

View File

@@ -1,31 +0,0 @@
import React, { Component } from "react";
import { Modal, Icon, Button, Header } from "semantic-ui-react";
class Message extends Component {
constructor(props) {
super(props);
}
_logout = event => {
event.preventDefault();
this.props.manualLogout();
};
render() {
return (
<Modal open={this.props.open} onClose={this.props.onClose} basic size="small">
<Header icon="info" content={this.props.title} />
<Modal.Content>
<p>{this.props.message}</p>
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.props.onClose} inverted>
<Icon name='checkmark' /> Got it
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default Message;

View File

@@ -1,57 +0,0 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Menu, Dropdown } from "semantic-ui-react";
const navStyle = {
marginBottom: "30px"
};
const blankStyle = {
marginBottom: "50px"
};
class Navigation extends Component {
constructor(props) {
super(props);
this._logout = this._logout.bind(this);
}
_logout = event => {
event.preventDefault();
this.props.manualLogout();
};
render() {
var profileLink = account_server + "/profile";
var billingLink = account_server + "/billing";
return (
<div>
{this.props.user.authenticated ? (
<Menu style={navStyle}>
<Menu.Item name="systems" header>
OpenMHz
</Menu.Item>
<Menu.Menu position="right">
{/* <Link to="/update-plans">
<Menu.Item name="systems">Update Plans</Menu.Item>
</Link> */}
<Dropdown item text={this.props.user.email}>
<Dropdown.Menu>
<Dropdown.Item href={profileLink}>Profile</Dropdown.Item>
{/* <Dropdown.Item href={billingLink}>Billing</Dropdown.Item> */}
<Dropdown.Item onClick={this._logout}>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Menu.Menu>
</Menu>
) : (
<Menu style={navStyle}>
<Menu.Item name="systems" header>
OpenMHz
</Menu.Item>
</Menu>
)}
</div>
);
}
}
export default Navigation;

View File

@@ -1,5 +0,0 @@
.field {
padding-top: 10px;
padding-left: 10px;
padding-right: 10px;
}

View File

@@ -1,11 +0,0 @@
export default {
parse: function parse(queryString) {
var query = {};
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
}

View File

@@ -1,12 +0,0 @@
import { combineReducers } from "redux"
import { connectRouter } from 'connected-react-router'
import user from "../User/user-reducer"
import system from "../System/system-reducer"
import billing from "../Billing/billing-reducer"
export default (history) => combineReducers({
router: connectRouter(history),
user,
system,
billing
})

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Button } from '@storybook/react/demo';
import InvoiceList from '../Billing/InvoiceList';
import 'semantic-ui-css/semantic.min.css'
storiesOf('Button', module)
.add('with text', () => (
<Button>Hello Button</Button>
))
.add('with some emoji', () => (
<Button><span role="img" aria-label="so cool">😀 😎 👍 💯</span></Button>
));
export const invoices = [{"amount_due":1500,"amount_paid":1500,"date":1548819212,"invoice_pdf":"https://pay.stripe.com/invoice/invst_BYlZ3MlhYMHEStbvdVJ6vddxgp/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548819083,"invoice_pdf":"https://pay.stripe.com/invoice/invst_ts0VnXKuxHWHce02yjhu0Cz455/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548818978,"invoice_pdf":"https://pay.stripe.com/invoice/invst_HSjke6BJAWGlZL8ggn3Jgb8pnT/pdf","paid":true,"status":"paid","description":null},{"amount_due":6500,"amount_paid":6500,"date":1548816924,"invoice_pdf":"https://pay.stripe.com/invoice/invst_joLNjLFGU3PIondKAAop6kJaSZ/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548772157,"invoice_pdf":"https://pay.stripe.com/invoice/invst_cSWcbq3UKUHxPQChSjkxRKx6gO/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548772077,"invoice_pdf":"https://pay.stripe.com/invoice/invst_1aMYlwnPOdXKbebGuCjQfUrsg2/pdf","paid":true,"status":"paid","description":null}];
storiesOf('InvoiceList', module).add("standard", () => {
return (<InvoiceList invoices={invoices}/>)
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,60 +0,0 @@
import path from "path"
import express from "express"
import session from "express-session"
import bodyParser from "body-parser"
import connectMongo from "connect-mongo"
import secrets from "./secrets"
const MongoStore = connectMongo(session)
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
export default function(app, passport) {
app.set("port", 3009)
// X-Powered-By header has no functional value.
// Keeping it makes it easier for an attacker to build the site's profile
// It can be removed safely
app.disable("x-powered-by")
app.enable('trust proxy')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(express.static(path.join(process.cwd(), 'public')));
const sess = {
secret: 'secret',
/*saveUninitialized: false,
resave: false,*/
resave: true,
saveUninitialized: true,
secret: secrets.sessionSecret,
proxy: true,
name: "sessionId",
cookie: {
httpOnly: true,
secure: false,
domain: cookie_domain
},
store: new MongoStore({
url: secrets.db,
autoReconnect: true
})
}
var node_env = process.env.NODE_ENV;
console.log('--------------------------');
console.log('===> 😊 Starting Server . . .');
console.log('===> Environment: ' + node_env);
/*if(node_env === 'production') {
console.log('===> 🚦 Note: In order for authentication to work in production');
console.log('===> you will need a secure HTTPS connection');
sess.cookie.secure = true; // Serve secure cookies
sess.cookie.httpOnly = false;
}*/
app.use(session(sess))
app.use(passport.initialize())
app.use(passport.session())
}

View File

@@ -1,35 +0,0 @@
/*
Configuring local strategy to authenticate strategies
Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js
*/
import mongoose from "mongoose"
import { Strategy as LocalStrategy } from "passport-local"
import User from "../../models/user"
/*
By default, LocalStrategy expects to find credentials in parameters named username and password.
If your site prefers to name these fields differently, options are available to change the defaults.
*/
export default new LocalStrategy({
usernameField: "email"
}, (email, password, done) => {
User.findOne({
$or: [{
email
}, {
local: {
email
}
}]
}, (err, user) => {
if(!user) return done(null, false, { message: `Email ${email} not found` })
user.comparePassword(password, (err, isMatch) => {
if (isMatch) {
return done(null, user)
} else {
return done(null, false, { message: "Invalid email or password" })
}
})
})
})

View File

@@ -1,25 +0,0 @@
/* Initializing PassportJS */
import User from "../models/user"
import local from "./passport-strategies/local"
export default function (app, passport) {
// Configure Passport authenticated session persistence.
//
// In order to restore authentication state across HTTP requests, Passport needs
// to serialize users into and deserialize users out of the session. The
// typical implementation of this is as simple as supplying the user ID when
// serializing, and querying the user record by ID from the database when
// deserializing.
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user)
})
})
// use the following strategies
passport.use(local)
}

View File

@@ -1,15 +0,0 @@
/** Important **/
/** You should not be committing this file to GitHub **/
/** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! **/
var host = process.env['MONGO_NODE_DRIVER_HOST'] != null ? process.env['MONGO_NODE_DRIVER_HOST'] : 'localhost';
var port = process.env['MONGO_NODE_DRIVER_PORT'] != null ? process.env['MONGO_NODE_DRIVER_PORT'] : 27017;
var mongoUrl = 'mongodb://' + host + ':' + port + '/scanner';
const secrets = {
db: mongoUrl,
sessionSecret: "letthisbeyoursecret"
}
export default secrets

View File

@@ -1,272 +0,0 @@
import mongoose from "mongoose";
import Plan from "../models/plan";
import System from "../models/system";
const stripeKeyPublishable = process.env.STRIPE_PUBLISHABLE_KEY;
const stripeKeySecret = process.env.STRIPE_SECRET_KEY;
const stripe = require("stripe")(stripeKeySecret);
var admin_email = process.env['ADMIN_EMAIL'] != null ? process.env['ADMIN_EMAIL'] : "luke@openmhz.com";
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : "https://account.openmhz.com";
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
exports.isLoggedIn = function (req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect("/login");
};
function makeSystemsResponse(systems) {
var returnSys = systems.map(obj => {
var rObj = (({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
planType,
showScreenName
}) => ({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
planType,
showScreenName
}))(obj);
rObj.id = obj._id;
return rObj;
});
return returnSys;
}
function findSubscriptionItem(subscription, planId) {
var items = subscription.items.data;
var subscriptionItem = items.find(function (element) {
return element.plan.id == planId;
});
return subscriptionItem;
};
// -------------------------------------------
// makes sure the plans a user is trying to update actually belong to them.
function validatePlans(systems, updatedPlans) {
var validatedPlans = [];
systems.forEach(function (system) {
// Find the updated plan for that system.
const updatedPlan = updatedPlans.find(function (element) {
return element.shortName == system.shortName;
})
if (!updatedPlan) {
console.log("ERROR: updated plan not found for ShortName");
}
// Update that systems plan type
// TODO: add validation of plantype
system.planType = updatedPlan.planType;
validatedPlans.push({ shortName: system.shortName, planType: updatedPlan.planType });
});
return validatedPlans;
}
exports.updatePlans = async function (req, res, next) {
const userId = mongoose.Types.ObjectId(req.user._id);
if (!req.user.customerId) {
console.log("ERROR: user does not have billing setup");
res.json({
success: false,
message: "ERROR: user does not have billing setup"
});
return;
}
const updatedPlans = req.body.updatePlans;
console.log(updatedPlans);
try {
var plan = await Plan.findOne({ userId: userId });
// looks like the User does not have a plan yet
if (!plan) {
plan = new Plan({ userId: userId });
}
// Find all of the Users Systems
var systems = await System.find({ userId: mongoose.Types.ObjectId(req.user._id) })
} catch (err) {
console.log("Error upate plans: " + err);
res.json({
success: false,
message: err
});
return;
}
// For each of the User's Systems
var validatedPlans = validatePlans(systems, updatedPlans);
// Create the subscription object
console.log("---------Validated Plans--------");
console.log(validatedPlans);
var proPlans = { plan: "plan_EQoJeqkAw6FCgy", quantity: 0 };
var proPlanSystems = [];
var proPlanQuantity = 0;
validatedPlans.forEach(function (plan) {
if (plan.planType == 10) {
proPlanQuantity++;
proPlans.quantity++;
proPlanSystems.push(plan.shortName);
}
});
if (!plan.subscriptionId) {
//proPlans.metadata = proPlanSystems.join();
var items = [];
items.push(proPlans);
//Subscribe the User
console.log(subscription);
try {
var subscription = await stripe.subscriptions.create({
customer: req.user.customerId,
items: items
});
console.log(subscription);
const subscriptionItem = findSubscriptionItem(subscription, "plan_EQoJeqkAw6FCgy");
if (subscriptionItem) {
plan.proSystemSubscription = { subscriptionItemId: subscriptionItem.id, quantity: proPlanQuantity, systems: proPlanSystems };
plan.subscriptionId = subscription.id;
await plan.save();
systems.forEach(async function (system) { await system.save(); });
// Save the updated Systems
const returnSystems = makeSystemsResponse(systems);
res.json({
success: true,
systems: returnSystems
});
return;
}
} catch (err) {
console.log("Error upate plans: " + err);
res.json({
success: false,
message: err
});
return;
}
} else {
// we alreay have a Subscription for the User
try {
var subscriptionItem=null;
// Check if the user has a Pro Plan subscription Item setup yet.
if (plan.proSystemSubscription) {
subscriptionItem = await stripe.subscriptionItems.update(plan.proSystemSubscription.subscriptionItemId,
{
quantity: proPlanQuantity,
});
} else {
// Create one if they don't
subscriptionItem = await stripe.subscriptionItems.create(
{
subscription: plan.subscriptionId,
plan: "plan_EQoJeqkAw6FCgy",
quantity: proPlanQuantity,
});
}
if (subscriptionItem) {
plan.proSystemSubscription = { subscriptionItemId: subscriptionItem.id, quantity: proPlanQuantity, systems: proPlanSystems };
await plan.save();
systems.forEach(async function (system) { await system.save(); });
// Save the updated Systems
const returnSystems = makeSystemsResponse(systems);
res.json({
success: true,
systems: returnSystems
});
return;
}
} catch (err) {
console.log("Error upate plans: " + err);
res.json({
success: false,
message: err
});
return;
}
}
}
exports.getInvoices = async function (req, res, next) {
if (!req.user.customerId) {
console.log("ERROR: user does not have billing setup");
res.json({
success: false,
message: "ERROR: user does not have billing setup"
});
return;
}
try {
console.log("Trying to fetch invoices for: " + req.user.customerId);
var invoices = await stripe.invoices.list( { limit: 12, customer: req.user.customerId });
console.log(invoices);
var obj = invoices.data.map(obj => {
var rObj = (({
amount_due,
amount_paid,
date,
invoice_pdf,
paid,
status,
description
}) => ({
amount_due,
amount_paid,
date,
invoice_pdf,
paid,
status,
description
}))(obj);
rObj.id = obj._id;
return rObj;
});
return res.json({
success: true,
invoices: obj
});
} catch (err) {
console.log("Error retrieving licesnes: " + err);
res.json({
success: false,
message: err
});
return;
}
}

View File

@@ -1,62 +0,0 @@
import mongoose from "mongoose";
import System from "../models/system";
// -------------------------------------------
exports.getUserSystems = function(_id) {
console.log("Listing Systems for: " + _id);
System.find(
{
userId: mongoose.Types.ObjectId(_id)
},
function(err, systems) {
if (err) {
console.error("Error - ListSystems: " + err);
return({ success: false, message: err });
}
var returnSys = systems.map(obj => {
var rObj = (({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
planType,
showScreenName
}) => ({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
planType,
showScreenName
}))(obj);
rObj.id = obj._id;
return rObj;
});
return({ success: true, systems: returnSys });
}
);
}
exports.listSystems = function(req, res, next) {
return listSystems(req.user._id);
}

View File

@@ -1,606 +0,0 @@
import mongoose from "mongoose";
import passport from "passport";
import User from "../models/user";
import secrets from "../config/secrets";
const mailjet = require("node-mailjet").connect(
process.env['MAILJET_KEY'],
process.env['MAILJET_SECRET']
);
const crypto = require("crypto");
var admin_email = process.env['ADMIN_EMAIL'] != null ? process.env['ADMIN_EMAIL'] : "luke@openmhz.com";
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : "https://account.openmhz.com";
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
exports.isLoggedIn = function(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect("/login");
};
// -------------------------------------------
exports.authenticated = function(req, res, next) {
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
console.log("auth: " + req.isAuthenticated());
console.log("req.user: " + req.user);
if (req.isAuthenticated()) {
var clientUser = (({
firstName,
lastName,
screenName,
location,
email,
admin
}) => ({
firstName,
lastName,
screenName,
location,
email,
admin
}))(
req.user
);
clientUser.userId = req.user.id
if (req.user.customerId) {
clientUser.billing = true;
} else {
clientUser.billing = false;
}
return res.json({
success: true,
user: clientUser
});
} else {
return res.json({
success: false
});
}
}
exports.login = function(req, res, next) {
// Do email and password validation for the server
passport.authenticate("local", function(err, user, info) {
if (err) return next(err);
if (!user) {
return res.json({
success: false,
message: info.message
});
}
// ***********************************************************************
// "Note that when using a custom callback, it becomes the application's
// responsibility to establish a session (by calling req.login()) and send
// a response."
// Source: http://passportjs.org/docs
// ***********************************************************************
// Passport exposes a login() function on req (also aliased as logIn())
// that can be used to establish a login session
req.login(user, loginErr => {
if (loginErr) {
return res.json({
success: false,
message: loginErr
});
}
if (!user.confirmEmail) {
req.logout();
return res.json({
success: false,
message: "unconfirmed email",
userId: user.id
});
}
console.log(user);
// go ahead and create the new user
var clientUser = (({
firstName,
lastName,
screenName,
location,
email,
admin
}) => ({
firstName,
lastName,
screenName,
location,
email,
admin
}))(
user
);
clientUser.userId = user.id
return res.json({
success: true,
message: "authentication succeeded",
user: clientUser,
userId: user.id
});
});
})(req, res, next);
};
exports.confirmEmail = function(req, res, next) {
const userId = req.params["userId"];
const token = req.params["token"];
User.findById(userId, (err, user) => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
if (user.confirmEmail) {
console.log("User already confirmed email: " + userId);
res.json({
success: false,
message: "already confirmed email"
});
return;
}
const today = new Date();
if (user.confirmEmailTTL < today) {
res.json({
success: false,
message: "token expired"
});
return;
}
if (user.confirmEmailToken != token) {
console.log(
"Token Mismatch DB: " + user.confirmEmailToken + " submitted: " + token
);
res.json({
success: false,
message: "token mismatch"
});
return;
}
user.confirmEmail = true;
user.confirmEmailToken = "";
user.save(err => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
console.log("User: " + user.email + " confirmed email address");
res.json({
success: true
});
return;
});
});
};
exports.resetPassword = function(req, res, next) {
const userId = req.params["userId"];
const token = req.params["token"];
User.findById(userId, (err, user) => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
const today = new Date();
if (user.resetPasswordTTL < today) {
res.json({
success: false,
message: "token expired"
});
return;
}
if (user.resetPasswordToken != token) {
console.log(
"Token Mismatch DB: " + user.resetPasswordToken + " submitted: " + token
);
res.json({
success: false,
message: "token mismatch"
});
return;
}
user.password = req.body.password
user.confirmEmail = true;
user.confirmEmailToken = "";
user.resetPasswordToken = "";
user.save(err => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
res.json({
success: true
});
return;
});
});
};
// -------------------------------------------
exports.sendResetPassword = function(req, res, next) {
User.findOne({
email: req.body.email
}, (err, user) => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
if (!user) {
console.error("Reset password failed. No user: " + req.body.email);
res.json({
success: false,
message: "No account register for " + req.body.email
});
return;
}
const buffer = crypto.randomBytes(20);
const token = buffer.toString("hex");
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
user.resetPasswordToken = token;
user.resetPasswordTTL = tomorrow;
user.save(err => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
const request = mailjet.post("send", {
version: "v3.1"
}).request({
Messages: [{
From: {
Email: admin_email,
Name: site_name + " Admin"
},
To: [{
Email: user.email,
Name: user.firstName + " " + user.lastName
}],
Subject: site_name + " - Password Reset",
TextPart: "It looks like you may have forgot your password. Copy this link to your browser to reset your password. Let us know if you are receiving this but did not request a password reset. /r" + account_server + "/reset-password/" + user.id + "/" + token,
HTMLPart: "<h3>Thanks for using " + site_name + "!</h3><br />It looks like you may have forgot your password. Copy this link to your browser to reset your password. Let us know if you are receiving this but did not request a password reset.<p>" + account_server + "/reset-password/" + user.id + "/" + token + "</p>"
}]
});
request
.then(result => {
console.log(`Password reset sent to: ${user.email}`)
})
.catch(err => {
console.error(err.statusCode);
console.error(err);
res.json({
success: false,
message: err
});
return;
});
res.json({
success: true
});
return;
});
});
};
function handleSendConfirmEmail(user) {
return new Promise((resolve, reject) => {
if (user.confirmEmail) {
console.log("Error - Send Confirm Email - User already confirmed email: " + user.email);
reject({
success: false,
message: "already confirmed email"
});
return;
}
const buffer = crypto.randomBytes(20);
const token = buffer.toString("hex");
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
user.confirmEmailToken = token;
user.confirmEmailTTL = tomorrow;
user.save().then(function(user) {
return mailjet.post("send", {
version: "v3.1"
}).request({
Messages: [{
From: {
Email: admin_email,
Name: site_name + " Admin"
},
To: [{
Email: user.email,
Name: user.firstName + " " + user.lastName
}],
Subject: "Confirm " + site_name + " Account",
TextPart: "Thanks for signing up for " + site_name + ". We just wanted to check and make sure your email address was real. Copy and paste this addres in your browser to confirm your email address: " + account_server + "/confirm-email/" + user._id + "/" + token,
HTMLPart: "<h3>Thanks for signing up for " + site_name + "</h3><br />We just wanted to check and make sure your email address was real. Copy and paste this addres in your browser to confirm your email address:<p> " + account_server + "/confirm-email/" + user._id + "/" + token + "</p>"
}]
});
}).then(result => {
console.log("Confirm email sent to: " + user.email);
resolve({
success: true,
userId: user._id
});
}).catch(err => {
console.error("Admin Email: " + admin_email + " User Email: " + user.email);
console.error("Error - Send Confirm Email - caught: " + err);
reject({
success: false,
message: err
});
});
});
};
// -------------------------------------------
exports.sendConfirmEmail = function(req, res, next) {
const userId = req.params["userId"];
User.findById(userId, (err, user) => {
if (err) {
console.error("Error - Send Confirm Email: " + err);
res.json({
success: false,
message: err
});
return;
}
if (!user) {
console.error("Error - Send Confirm Email: User not found " + userId);
res.json({
success: false,
message: "user not found"
});
return;
}
handleSendConfirmEmail(user).then(function(result) {
res.json(result);
return;
}).catch(function(error) {
res.json(error);
return;
});
});
};
// -------------------------------------------
exports.logout = function(req, res, next) {
// the logout method is added to the request object automatically by Passport
req.logout();
res.clearCookie('sessionId', {domain: cookie_domain, path:'/'});
return res.json({
success: true
});
};
exports.terms = function(req, res, next) {
const userId = req.params["userId"];
User.findById(userId, (err, user) => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
if (req.user.id != userId) {
console.log(
"Logged in user's ID: " +
req.user.id +
" does not match Param: " +
userId
);
res.json({
success: false,
message: "UserID incorrect"
});
return;
}
user.terms = 1.1; //User.termsVer;
user.save(err => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
res.json({
success: true
});
return;
});
});
};
// -------------------------------------------
exports.validateProfile = function(req, res, next) {
if (!req.body.firstName || (req.body.firstName.length < 2)) {
console.error("ERROR: Validate System - req.body.firstName");
res.json({
success: false,
message: "First Name is Required"
});
return;
}
res.locals.firstName = req.body.firstName.replace(/[^\w\s\.\,\-\'\`]/gi, '');
if (!req.body.lastName || (req.body.lastName.length < 2)) {
console.error("ERROR: Validate System - req.body.lastName");
res.json({
success: false,
message: "Last Name is Required"
});
return;
}
res.locals.lastName = req.body.lastName.replace(/[^\w\s\.\,\-\'\`]/gi, '');
res.locals.screenName = req.body.screenName.replace(/[^\w\s\.\,\-\_]/gi, '');
if (!req.body.location || (req.body.location.length < 2)) {
console.error("ERROR: Validate System - req.body.location");
res.json({
success: false,
message: "System location is Required"
});
return;
}
res.locals.location = req.body.location.replace(/[^\w\s\.\,\-\_]/gi, '');
next();
}
exports.updateProfile = function(req, res, next) {
const userId = req.params["userId"];
User.findById(userId, (err, user) => {
// is email address already in use?
if (req.user.id != userId) {
console.log(
"ERROR: Logged in user's ID: " +
req.user.id +
" does not match Param: " +
userId
);
res.json({
success: false,
message: "UserID incorrect"
});
return;
}
// go ahead and create the new user
user.firstName = res.locals.firstName
user.lastName = res.locals.lastName
user.screenName = res.locals.screenName
user.location = res.locals.location
console.log(user)
user.save(err => {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
var clientUser = (({
firstName,
lastName,
screenName,
location
}) => ({
firstName,
lastName,
screenName,
location
}))(
user
);
console.log("Updated Profile: " + userId)
res.json({
success: true,
user: clientUser
});
return;
});
});
};
// -------------------------------------------
exports.register = function(req, res, next) {
User.findOne({
$or: [{
email: req.body.email
}, {
local: {
email: req.body.email
}
}]
}, (err, user) => {
// is email address already in use?
if (user) {
res.json({
success: false,
message: "Email already in use"
});
return;
}
// go ahead and create the new user
var user = (({
firstName,
lastName,
screenName,
location,
email,
password
}) => ({
firstName,
lastName,
screenName,
location,
}))(
res.locals
);
user.password = req.body.password;
user.email = req.body.email;
User.create(user).then(function(dbUser) {
console.log(dbUser);
// Since regiestration worked, send a confirmation email.
return handleSendConfirmEmail(dbUser);
}).then(function(result) {
res.json(result);
return;
}).catch(function(err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
});
});
};

View File

@@ -1,280 +0,0 @@
import fs from "fs"
import express from "express"
import mongoose from "mongoose"
import passport from "passport"
import webpack from "webpack"
import path from "path"
import config from "../../webpack/webpack.config.dev.js"
import secrets from "./config/secrets"
import configurePassport from "./config/passport"
import configureExpress from "./config/express"
import users from "./controllers/users"
import plans from "./controllers/plans"
const stripeKeyPublishable = process.env.STRIPE_PUBLISHABLE_KEY;
const stripeKeySecret = process.env.STRIPE_SECRET_KEY;
const stripe = require("stripe")(stripeKeySecret);
import "./models/user"
// -------------------------------------------
const app = express()
// -------------------------------------------
const connect = () => {
mongoose.connect(secrets.db, (err, res) => {
if (err) {
console.log(`Error connecting to ${secrets.db}. ${err}`)
} else {
console.log(`Successfully connected to ${secrets.db}.`)
}
})
}
connect()
mongoose.connection.on("error", console.error)
mongoose.connection.on("disconnected", connect)
// -------------------------------------------
const isDev = process.env.NODE_ENV === "development"
// if in development mode set up the middleware required for hot reloading and rebundling
if(isDev) {
const compiler = webpack(config)
app.use(require("webpack-dev-middleware")(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}))
app.use(require("webpack-hot-middleware")(compiler))
}
var backend_server = process.env['BACKEND_SERVER'] != null ? process.env['BACKEND_SERVER'] : 'https://api.openmhz.com';
var frontend_server = process.env['FRONTEND_SERVER'] != null ? process.env['FRONTEND_SERVER'] : 'https://openmhz.com';
var socket_server = process.env['SOCKET_SERVER'] != null ? process.env['SOCKET_SERVER'] : 'wss://api.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
var admin_server = process.env['ADMIN_SERVER'] != null ? process.env['ADMIN_SERVER'] : 'https://admin.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
const proPlanValue = process.env['PRO_PLAN'] != null ? process.env['PRO_PLAN'] : 10;
const freePlanValue = process.env['FREE_PLAN'] != null ? process.env['FREE_PLAN'] : 0;
const proPlanPrice = process.env['PRO_PLAN_PRICE'] != null ? process.env['PRO_PLAN_PRICE'] : 15;
const freePlanPrice = process.env['FREE_PLAN_PRICE'] != null ? process.env['FREE_PLAN_PRICE'] : 0;
// -------------------------------------------
configurePassport(app, passport)
configureExpress(app, passport)
app.all('*', function(req, res, next) {
var allowedOrigins = [account_server, admin_server, frontend_server];
var origin = req.headers.origin;
if(allowedOrigins.indexOf(origin) > -1){
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
});
// -------------------------------------------
app.use(express.static(path.join(__dirname, "public")));
app.post("/login", users.login)
app.get("/logout", users.logout)
app.get("/authenticated", users.authenticated)
app.post("/register", users.validateProfile, users.register)
app.post("/users/:userId/reset-password/:token", users.resetPassword)
app.post("/api/send-reset-password", users.sendResetPassword)
app.post("/users/:userId", users.isLoggedIn, users.validateProfile, users.updateProfile)
app.post("/users/:userId/terms", users.isLoggedIn, users.terms)
app.post("/users/:userId/send-confirm", users.sendConfirmEmail)
app.post("/users/:userId/confirm/:token", users.confirmEmail)
/*app.get("/api/invoices", users.isLoggedIn, plans.getInvoices)
app.post("/api/plans", users.isLoggedIn, plans.updatePlans)
app.get("/api/billing", users.isLoggedIn, (req,res) => {
if (req.user.customerId) {
stripe.customers.retrieve(
req.user.customerId,
function(err, customer) {
if (err) {
return res.json({
success: false,
message: err
});
}
if (customer) {
if (customer.sources.total_count < 1) {
return res.json({
success: false,
message: "Payment sources not defined"
});
}
for (var i=0; i < customer.sources.data.length; i++) {
var source = customer.sources.data[i];
if (source.id == customer.default_source) {
console.log("match on: " + i);
var obj = {
name: source.owner.name,
brand: source.card.brand,
country: source.card.country,
exp_month: source.card.exp_month,
exp_year: source.card.exp_year,
last4: source.card.last4
}
return res.json({
success: true,
source: obj
});
}
}
}
return res.json({
success: false,
message: "Default payment source not found"
});
}
);
} else {
return res.json({
success: false,
message: "User does not have a Customer ID"
});
}
});*/
function updateCustomerId(req, res, err, customer) {
if (err){
return res.json({
success: false,
message: err
});
}
req.user.customerId = customer.id;
req.user.save(err => {
if (err) {
console.error("Error saving customer ID");
console.error(err);
res.json({
success: false,
message: err
});
return;
}
});
console.log(customer);
if (customer.sources.total_count < 1) {
return res.json({
success: false,
message: "Payment sources not defined"
});
}
for (var i=0; i < customer.sources.data.length; i++) {
console.log("--------Customer-----------");
console.log(customer);
var source = customer.sources.data[i];
if (source.id == customer.default_source) {
console.log("--------Source-----------");
console.log(source);
var obj = {
name: source.owner.name,
brand: source.card.brand,
country: source.card.country,
exp_month: source.card.exp_month,
exp_year: source.card.exp_year,
last4: source.card.last4
}
return res.json({
success: true,
source: obj
});
}
}
}
app.post("/api/customer", users.isLoggedIn, (req, res) => {
//
if (req.user.customerId) {
stripe.customers.update(req.user.customerId, {
source: req.body.sourceId
}, function(err, customer) {
return updateCustomerId(req, res, err, customer);
});
} else {
stripe.customers.create({
email: req.user.email,
source: req.body.sourceId
}, function(err, customer) {
return updateCustomerId(req, res, err, customer);
});
}
});
app.get("*", (req, res, next) => {
// if we are in production mode then an extension will be provided, usually ".min"
const minified = process.env.MIN_EXT || ""
// this is the HTML we will send to the client when they request any page. React and React Router
// will take over once the scripts are loaded client-side
const appHTML =
`<!doctype html>
<html lang="">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>OpenMHz - Account</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://js.stripe.com/v3/"></script>
<link rel="stylesheet" type="text/css" href="/style/style.css"></link>
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.css" />
<script>
const frontend_server = "${frontend_server}";
const admin_server = "${admin_server}";
const backend_server = "${backend_server}";
const socket_server = "${socket_server}";
const account_server = "${account_server}";
const site_name = "${site_name}";
const proPlanValue = ${proPlanValue};
const freePlanValue = ${freePlanValue};
const proPlanPrice = ${proPlanPrice};
const freePlanPrice = ${freePlanPrice};
</script>
<style>
body {
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="/assets/app${minified}.js"></script>
</body>
</html>`
res.status(200).end(appHTML)
})
// start listening to incoming requests
app.listen(app.get("port"), app.get("host"), (err) => {
if (err) {
console.err(err.stack)
} else {
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
}
})

View File

@@ -1,23 +0,0 @@
// Defining a User Model in mongoose
// Code modified from https://github.com/sahat/hackathon-starter
import bcrypt from "bcrypt-nodejs"
import mongoose from "mongoose"
var SubscriptionSchema = mongoose.Schema({ subscriptionItemId: String, quantity: {type: Number, default: 0}, systems: [String] });
const PlanSchema = new mongoose.Schema({
userId: mongoose.Schema.Types.ObjectId,
userPlan: {
type: Number,
default: 0
},
subscriptionId: String,
proSystemSubscription: SubscriptionSchema
})
export default mongoose.model("Plan", PlanSchema)

View File

@@ -1,27 +0,0 @@
var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');
var systemSchema = mongoose.Schema({
name: String,
shortName: String,
systemType: String,
county: String,
country: String,
city: String,
state: String,
description: String,
showScreenName: Boolean,
planType: {type: Number, default: 0},
userId: mongoose.Schema.Types.ObjectId,
key: String
});
systemSchema.methods.generateHash = function(password) {
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
};
systemSchema.methods.validPassword = function(password) {
return bcrypt.compareSync(password, this.local.password);
};
module.exports = mongoose.model('System', systemSchema);

View File

@@ -1,92 +0,0 @@
// Defining a User Model in mongoose
// Code modified from https://github.com/sahat/hackathon-starter
import bcrypt from "bcrypt-nodejs"
import mongoose from "mongoose"
import crypto from "crypto"
const UserSchema = new mongoose.Schema({
local: {
name: String,
email: String,
password: String
},
email: {
type: String,
unique: true,
lowercase: true
},
password: String,
screenName: {
type: String,
unique: true,
lowercase: true
},
firstName: String,
lastName: String,
location: String,
email: String,
resetPasswordToken: String,
resetPasswordTTL: Date,
confirmEmail: {
type: Boolean,
default: false
},
customerId: {
type: String,
default: ""
},
confirmEmailToken: String,
confirmEmailTTL: Date,
admin: {
type: Boolean,
default: false
},
terms: {
type: Number,
default: 0
},
ver: {
type: Number,
default: 1.1
},
sysCount: Number,
lastLogin: { type : Date, default: Date.now }
})
/**
* Password hash middleware.
*/
UserSchema.pre("save", function(next) {
var user = this
if (!user.isModified("password")) return next()
bcrypt.genSalt(8, (err, salt) => {
if (err) return next(err)
bcrypt.hash(user.password, salt, null, (err, hash) => {
if (err) return next(err)
user.password = hash
user.local.password = hash;
next()
})
})
})
/*
Defining our own custom document instance method
*/
UserSchema.methods = {
comparePassword: function(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.local.password, (err, isMatch) => {
if (err) return cb(err)
cb(null, isMatch)
})
}
}
/**
* Statics
*/
UserSchema.statics = {}
export default mongoose.model("User", UserSchema)

View File

@@ -1,30 +0,0 @@
.field {
padding-top: 10px;
padding-left: 5px;
padding-right: 5px;
}
/**
* The CSS shown here will not be introduced in the Quickstart guide, but shows
* how you can use CSS to style your Element's container.
*/
.StripeElement {
background-color: white;
height: 40px;
padding: 10px 12px;
border-radius: 5px;
border: 1px solid rgba(34,36,38,.15);
color: rgba(0,0,0,.87);
}
.StripeElement--focus {
border-color: #85b7d9;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}

View File

@@ -1,18 +0,0 @@
db = db.getMongo().getDB( "scanner" );
db.users.find().forEach(
function(user){
db.systems.updateMany(
{ "user": { $eq: user.email } },
{
$set: { "userId": user._id, "showScreenName": false },
}
);
print("Upgrading: " + user.email);
});

View File

@@ -1,56 +0,0 @@
var path = require("path")
var webpack = require("webpack")
var assetsPath = path.join(__dirname, "..", "public", "assets")
var publicPath = "/assets/"
module.exports = {
name: "browser",
mode: 'development',
devtool: "eval-source-map",
// The base directory (absolute path!) for resolving the entry option.
context: path.join(__dirname, "..", "src", "client"),
entry: {
app: [
"./client",
"webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true"
]
},
optimization: {
minimize: false
},
output: {
path: assetsPath,
filename: "[name].js",
publicPath: publicPath
},
module: {
rules: [{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
}],
},
resolve: {
extensions: [".js", ".jsx"],
modules: [
path.resolve('src'),
'node_modules',
],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("development")
}
})
]
}

View File

@@ -1,51 +0,0 @@
var path = require("path")
var webpack = require("webpack")
var assetsPath = path.join(__dirname, "..", "public", "assets")
var publicPath = "/assets/"
module.exports = {
// The configuration for the client
name: "browser",
mode: 'production',
devtool: "source-map",
// The base directory (absolute path!) for resolving the entry option.
context: path.join(__dirname, "..", "src", "client"),
optimization: {
minimize: true
},
entry: {
app: "./client"
},
output: {
path: assetsPath,
filename: "[name].min.js",
publicPath: publicPath
},
resolve: {
extensions: [".js", ".jsx"],
modules: [
path.resolve('src'),
'node_modules',
],
},
module: {
rules: [{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
}],
},
plugins: [
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("production")
}
})
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
{
"presets":
["@babel/preset-env",
"@babel/react"],
"plugins": [
"@babel/plugin-proposal-class-properties",
["@babel/plugin-transform-runtime", {
"regenerator": true
}],
["lodash", { "id": ["lodash", "semantic-ui-react"] }]
]
}

View File

@@ -1,3 +0,0 @@
node_modules
lib
public

36
admin-old/.gitignore vendored
View File

@@ -1,36 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
lib
uploads/
# Dependency directory
node_modules
assets
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history

View File

@@ -1,21 +0,0 @@
# docker build -t trunk-server-frontend .
# do a `git pull` in trunk-server-frontend to update
FROM node:10
ENV HOME=/home/app
RUN mkdir -p /home/app
COPY ./package.json /tmp
RUN cd /tmp && npm install
RUN cp -a /tmp/node_modules /home/app
# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/app/node_modules/.bin:$PATH
WORKDIR $HOME
COPY . $HOME/
RUN npm run build
CMD ["npm", "run", "start"]

View File

View File

@@ -1,10 +0,0 @@
/*
Called during development only. Allows us to use ES6 and JSX in our server code.
*/
require('@babel/register')({
presets: ["@babel/preset-env",
"@babel/react"]
});
require("./src/server/index")

File diff suppressed because it is too large Load Diff

View File

@@ -1,94 +0,0 @@
{
"name": "react-passport-redux-example",
"version": "1.0.0",
"description": "Demonstration of PassportJS authentication in React w/ Redux.",
"main": "devStart.js",
"scripts": {
"build:lib": "babel src/server --out-dir lib/server",
"build:umd": "webpack --config webpack/webpack.config.dev.js",
"build:umd:min": "webpack --config webpack/webpack.config.prod.js",
"build": "npm run clean && npm run build:lib && npm run build:umd && npm run build:umd:min",
"postinstall": "npm run build",
"clean": "rimraf lib public/assets",
"server": "better-npm-run server",
"server:dev": "better-npm-run server:dev",
"start": "npm run server",
"start:dev": "npm run server:dev"
},
"betterScripts": {
"server": {
"command": "node lib/server/index.js",
"env": {
"NODE_ENV": "production",
"MIN_EXT": ".min"
}
},
"server:dev": {
"command": "node devStart.js",
"env": {
"NODE_ENV": "development"
}
}
},
"dependencies": {
"@babel/runtime": "7.3.1",
"@vx/axis": "^0.0.184",
"@vx/curve": "^0.0.182",
"@vx/glyph": "^0.0.183",
"@vx/group": "^0.0.183",
"@vx/legend": "^0.0.183",
"@vx/mock-data": "^0.0.185",
"@vx/responsive": "0.0.184",
"@vx/scale": "0.0.182",
"@vx/shape": "0.0.184",
"@vx/tooltip": "0.0.184",
"axios": "^0.18.0",
"babel-loader": "8.0.5",
"bcrypt-nodejs": "^0.0.3",
"better-npm-run": "^0.1.0",
"body-parser": "^1.18.2",
"connect-mongo": "^2.0.1",
"connected-react-router": "6.3.1",
"csv-parse": "^4.3.3",
"d3-time-format": "^2.1.3",
"express": "^4.16.2",
"express-session": "^1.15.6",
"fs-extra": "^7.0.1",
"lodash": "4.17.11",
"mongoose": "^5.4",
"multer": "^1.3.0",
"node-mailjet": "*",
"node-uuid": "^1.4.8",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"react": "16.8.2",
"react-dom": "16.8.2",
"react-redux": "^6.0.0",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^4.0.1",
"redux-thunk": "^2.2.0",
"semantic-ui-react": "^0.85.0"
},
"devDependencies": {
"@babel/cli": "7.2.3",
"@babel/core": "7.3.3",
"@babel/plugin-proposal-class-properties": "7.3.3",
"@babel/plugin-proposal-export-default-from": "7.2.0",
"@babel/plugin-proposal-object-rest-spread": "7.3.2",
"@babel/plugin-transform-runtime": "7.2.0",
"@babel/preset-env": "7.3.1",
"@babel/preset-react": "^7.0.0",
"@babel/preset-stage-1": "^7.0.0",
"@babel/register": "^7.0.0",
"babel-plugin-lodash": "3.3.4",
"detect-node": "2.0.4",
"redux-logger": "^3.0.6",
"rimraf": "2.6.3",
"uglifyjs-webpack-plugin": "2.1.1",
"webpack": "4.29.5",
"webpack-cli": "3.2.3",
"webpack-dev-middleware": "3.5.2",
"webpack-hot-middleware": "2.24.3"
}
}

View File

@@ -1,258 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Form,
Grid,
Modal,
Rail,
Segment,
List,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class GroupModal extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
// Set initial state
this.state = {
editGroup: false,
groupName: "",
group: {
name: "",
items: []
},
talkgroups: this.props.talkgroups ? this.props.talkgroups : []
};
}
componentDidMount() {
//this.props.groupActions.fetchGroups(this.props.shortName);
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
}
componentWillReceiveProps(nextProps) {
if (nextProps.talkgroups && nextProps.groups && nextProps.editGroupId) {
var editGroup = nextProps.groups.find(
group => group._id === nextProps.editGroupId
);
var items = [];
var talkgroups = nextProps.talkgroups;
for (var i = 0; i < editGroup.talkgroups.length; i++) {
const talkgroupId = editGroup.talkgroups[i];
const index = talkgroups.findIndex(tg => tg.num === talkgroupId);
if (index >= 0) {
const item = {
num: talkgroups[index].num,
description: talkgroups[index].description
};
items.push(item);
talkgroups = talkgroups
.slice(0, index)
.concat(talkgroups.slice(index + 1));
}
}
const group = {name: editGroup.groupName, items: items}
this.setState({group: group, talkgroups: talkgroups, groupName: editGroup.groupName});
} else {
this.setState({ talkgroups: nextProps.talkgroups })
}
if (!nextProps.editGroupId && (this.props.editGroupId != nextProps.editGroupId)) {
this.setState({ groupName: "", group: {
name: "",
items: []
}});
}
}
removeTalkgroup(event, index) {
const item = {
num: this.state.group.items[index].num,
description: this.state.group.items[index].description
};
const talkgroups = this.state.talkgroups.slice(0); // copy
for (var i = 0, len = talkgroups.length; i < len; i++) {
if (item.num < talkgroups[i].num) {
talkgroups.splice(i, 0, item);
break;
}
}
this.setState({
group: {
items: this.state.group.items
.slice(0, index)
.concat(this.state.group.items.slice(index + 1))
},
talkgroups: talkgroups
});
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
addTalkgroup(event, index) {
const item = {
num: this.state.talkgroups[index].num,
description: this.state.talkgroups[index].description
};
this.setState({
group: { items: this.state.group.items.concat(item) },
talkgroups: this.state.talkgroups
.slice(0, index)
.concat(this.state.talkgroups.slice(index + 1))
});
}
handleClose = () => this.props.onClose(false);
handleSubmit() {
var talkgroupNums = this.state.group.items.map(a => a.num);
const data = {
shortName: this.props.shortName,
groupName: this.state.groupName,
talkgroups: JSON.stringify(talkgroupNums)
};
if (this.props.editGroupId) {
const data = {
_id: this.props.editGroupId,
shortName: this.props.shortName,
groupName: this.state.groupName,
talkgroups: JSON.stringify(talkgroupNums)
};
this.props.groupActions.updateGroup(data).then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
this.props.onClose(false);
} else {
this.props.onClose(true);
}
});
} else {
const data = {
shortName: this.props.shortName,
groupName: this.state.groupName,
talkgroups: JSON.stringify(talkgroupNums)
};
this.props.groupActions.createGroup(data).then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
this.props.onClose(false);
} else {
this.props.onClose(true);
}
});
}
}
render() {
const talkgroups = this.state.talkgroups;
const group = this.state.group.items;
const railStyle = {
top: "15px",
minHeight: "200px"
};
return (
<Modal open={this.props.open} onClose={this.handleClose} centered={false}>
<Modal.Header>
{this.props.editGroupId ? "Edit Group" : "Create Group"}
</Modal.Header>
<Modal.Content scrolling>
<Modal.Description>
<Grid container columns={2}>
<Grid.Column>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Number</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{talkgroups &&
talkgroups.map((talkgroup, i) => (
<Table.Row link="true" key={talkgroup.num + "-" + i}>
<Table.Cell>{talkgroup.num}</Table.Cell>
<Table.Cell>{talkgroup.description}</Table.Cell>
<Table.Cell>
<Icon
link
name="plus"
onClick={e => this.addTalkgroup(e, i)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<Rail position="right">
<Segment style={railStyle}>
<Input
name="groupName"
onChange={this.handleInputChange}
placeholder="Group Name"
value={this.state.groupName}
/>
<List divided relaxed>
{group.map((talkgroup, i) => (
<List.Item key={"Group-" + i}>
<List.Icon
name="remove"
verticalAlign="middle"
link={true}
onClick={e => this.removeTalkgroup(e, i)}
/>
<List.Content>{talkgroup.description}</List.Content>
</List.Item>
))}
</List>
<Button onClick={this.handleSubmit}>
<Icon name="checkmark" />{" "}
{this.props.editGroupId ? "Update" : "Save"}
</Button>
</Segment>
</Rail>
</Grid.Column>
</Grid>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleSubmit}>
<Icon name="checkmark" />{" "}
{this.props.editGroupId ? "Update" : "Save"}
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default GroupModal;

View File

@@ -1,26 +0,0 @@
import { connect } from "react-redux"
import * as groupActionCreators from "./group-actions"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import { bindActionCreators } from 'redux'
import GroupModal from "./GroupModal"
//groups: state.group.items[props.match.params.shortName],
function mapStateToProps(state,props) {
return {
talkgroups: state.talkgroup.items[props.shortName],
groups: state.group.items[props.shortName],
shortName: props.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators,dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(GroupModal)

View File

@@ -1,82 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
// ----------------------------------------------------
class ListGroups extends Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const groups = this.props.groups;
if (groups) {
return (
<Table >
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Talkgroup Count</Table.HeaderCell>
<Table.HeaderCell></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{groups.map((group, i) => (
<Table.Row key={ "Group-" + i}>
<Table.Cell>{group.groupName}</Table.Cell>
<Table.Cell>{group.talkgroups.length}</Table.Cell>
<Table.Cell>
<Icon
name="pencil alternate"
link={true}
onClick={e => this.props.editGroup(group._id)}
/><Icon
name="up arrow"
link={true}
onClick={e => this.props.reorderGroup(i, i-1)}
/><Icon
name="down arrow"
link={true}
onClick={e => this.props.reorderGroup(i, i+1)}
/><Icon
name="remove"
link={true}
onClick={e => this.props.deleteGroup(group._id)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
);
} else {
return (
<div/>
);
}
}
}
export default ListGroups;

View File

@@ -1,244 +0,0 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./group-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginCreateGroup() {
return { type: types.CREATE_GROUP };
}
function createGroupSuccess(data) {
return {
type: types.CREATE_SUCCESS_GROUP,
data
};
}
function createGroupError(data) {
return {
type: types.CREATE_ERROR_GROUP,
data
};
}
function beginUpdateGroup() {
return { type: types.UPDATE_GROUP };
}
function updateGroupSuccess(data) {
return { type: types.UPDATE_SUCCESS_GROUP, data };
}
function updateGroupError() {
return { type: types.UPDATE_ERROR_GROUP };
}
function beginDeleteGroup() {
return { type: types.DELETE_GROUP };
}
function deleteGroupSuccess(data) {
return { type: types.DELETE_SUCCESS_GROUP, data };
}
function deleteGroupError() {
return { type: types.DELETE_ERROR_GROUP };
}
function beginFetchGroup() {
return { type: types.FETCH_GROUP };
}
function fetchGroupSuccess(data) {
return {
type: types.FETCH_SUCCESS_GROUP,
data
};
}
function fetchGroupError(data) {
return {
type: types.FETCH_ERROR_GROUP,
data
};
}
function saveGroupOrderSuccess() {
return { type: types.SAVE_ORDER_SUCCESS_GROUP };
}
function saveGroupOrderError() {
return { type: types.SAVE_ORDER_ERROR_GROUP };
}
function makeUserRequest(method, data, api = "/login") {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
export function updateGroup(data) {
return dispatch => {
dispatch(beginUpdateGroup());
return makeUserRequest("post", data, "/groups/" + data.shortName + "/" + data._id)
.then(response => {
if (response.data.success) {
dispatch(updateGroupSuccess(response.data.group));
} else {
dispatch(updateGroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
function shouldFetchGroups(state,shortName) {
if (!state.group.items || !state.group.items[shortName]) {
return true;
} else if (state.group.isWaiting) {
return false;
} else {
return false;
}
}
export function reorderGroup(shortName, oldIndex, newIndex) {
return (dispatch, getState) => {
const data = {
shortName: shortName,
oldIndex: oldIndex,
newIndex: newIndex
};
dispatch({
type: types.REORDER_GROUP,
data
});
};
}
export function fetchGroups(shortName) {
return (dispatch, getState) => {
if (shouldFetchGroups(getState(), shortName)) {
dispatch(beginFetchGroup());
return axios
.get("/groups/" + shortName)
.then(response => {
if (response.data.success) {
const data = { shortName: shortName, groups: response.data.groups };
dispatch(fetchGroupSuccess(data));
} else {
dispatch(fetchGroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
}
};
}
export function saveGroupOrder(shortName, data) {
return dispatch => {
dispatch({
type: types.SAVE_ORDER_GROUP,
data
});
return makeUserRequest(
"post",
data,
"/groups/" + shortName + "/reorder"
)
.then(response => {
if (response.data.success) {
dispatch(saveGroupOrderSuccess());
} else {
dispatch(saveGroupOrderError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
export function deleteGroup(shortName, groupId) {
return dispatch => {
dispatch(beginDeleteGroup());
return makeUserRequest("delete", null, "/groups/" + shortName + "/" + groupId)
.then(response => {
if (response.data.success) {
const data = {shortName: shortName, groupId: groupId}
dispatch(deleteGroupSuccess(data));
} else {
dispatch(deleteGroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
export function createGroup(data) {
return dispatch => {
dispatch(beginCreateGroup());
return makeUserRequest("post", data, "/groups/" + data.shortName)
.then(response => {
if (response.data.success) {
dispatch(createGroupSuccess(response.data.group));
} else {
dispatch(createGroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}

View File

@@ -1,16 +0,0 @@
export const CREATE_GROUP = "CREATE_GROUP"
export const CREATE_SUCCESS_GROUP = "CREATE_SUCCESS_GROUP"
export const CREATE_ERROR_GROUP = "CREATE_ERROR_GROUP"
export const UPDATE_GROUP = "UPDATE_GROUP"
export const UPDATE_SUCCESS_GROUP = "UPDATE_SUCCESS_GROUP"
export const UPDATE_ERROR_GROUP = "UPDATE_ERROR_GROUP"
export const DELETE_GROUP = "DELETE_GROUP"
export const DELETE_SUCCESS_GROUP = "DELETE_SUCCESS_GROUP"
export const DELETE_ERROR_GROUP = "DELETE_ERROR_GROUP"
export const FETCH_GROUP = "FETCH_GROUP"
export const FETCH_SUCCESS_GROUP = "FETCH_SUCCESS_GROUP"
export const FETCH_ERROR_GROUP = "FETCH_ERROR_GROUP"
export const REORDER_GROUP = "REORDER_GROUP"
export const SAVE_ORDER_GROUP = "SAVE_ORDER_GROUP"
export const SAVE_ORDER_SUCCESS_GROUP = "SAVE_ORDER_SUCCESS_GROUP"
export const SAVE_ORDER_ERROR_GROUP = "SAVE_ORDER_ERROR_GROUP"

View File

@@ -1,128 +0,0 @@
import {
CREATE_GROUP,
CREATE_SUCCESS_GROUP,
CREATE_ERROR_GROUP,
DELETE_GROUP,
DELETE_SUCCESS_GROUP,
DELETE_ERROR_GROUP,
UPDATE_GROUP,
UPDATE_SUCCESS_GROUP,
UPDATE_ERROR_GROUP,
FETCH_GROUP,
FETCH_SUCCESS_GROUP,
FETCH_ERROR_GROUP,
REORDER_GROUP,
SAVE_ORDER_GROUP,
SAVE_ORDER_ERROR_GROUP,
SAVE_ORDER_SUCCESS_GROUP
} from "./group-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, updatedItem) {
const updatedItems = array.map(item => {
if(item._id !== updatedItem._id) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
return updatedItem;
});
return updatedItems;
}
const group = (
state = {
orderChange: false,
isWaiting: false,
items: {}},
action
) => {
switch (action.type) {
case SAVE_ORDER_GROUP:
case CREATE_GROUP:
case FETCH_GROUP:
case DELETE_GROUP:
case UPDATE_GROUP:
return Object.assign({}, state, { isWaiting: true});
case UPDATE_SUCCESS_GROUP:
const updatedGroups = updateItemInArray(state.items[action.data.shortName], action.data);
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: updatedGroups
}
});
case CREATE_SUCCESS_GROUP:
const newGroups = state.items[action.data.shortName].concat(action.data);
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: newGroups
}
});
case DELETE_SUCCESS_GROUP:
const delGroups = state.items[action.data.shortName].filter(x => x._id != action.data.groupId);
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: delGroups
}
});
case FETCH_SUCCESS_GROUP:
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: action.data.groups
}
});
case SAVE_ORDER_SUCCESS_GROUP:
return Object.assign({}, state, {
orderChange: false,
isWaiting: false
});
case SAVE_ORDER_ERROR_GROUP:
case FETCH_ERROR_GROUP:
case CREATE_ERROR_GROUP:
case DELETE_ERROR_GROUP:
case UPDATE_ERROR_GROUP:
return Object.assign({}, state, {
isWaiting: false
});
case REORDER_GROUP:
if ((action.data.newIndex<0)||(action.data.newIndex>(state.items[action.data.shortName].length-1))) {
return state
}
const moveGroups = state.items[action.data.shortName].slice();
moveGroups.splice(action.data.newIndex, 0, moveGroups.splice(action.data.oldIndex, 1)[0]);
return Object.assign({}, state, {
orderChange: true,
items: {
...state.items,
[action.data.shortName]: moveGroups
}
});
default:
return state;
}
};
export default group;

View File

@@ -1,150 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Checkbox,
Header,
Form,
Grid,
Modal,
Rail,
Segment,
List,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class AddPermissionModal extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleChange = this.handleChange.bind(this);
// Set initial state
this.state = {
role: 5,
email: "",
error: ""
};
}
componentDidMount() {
}
componentWillReceiveProps(nextProps) {
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleChange = (e, { newRole }) => this.setState({ newRole })
handleClose = () => {
this.setState({
error: ""
});
this.props.onClose(false);
}
handleSubmit() {
//if (this.props.updatePermissionId) {
const data = {email: this.state.email, shortName: this.props.shortName, role: this.state.role }
this.props.permissionActions.addPermission(data).then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
error: requestMessage
});
//this.props.onClose(false);
} else {
this.setState({
error: ""
});
this.props.onClose(true);
}
});
//}
}
render() {
const railStyle = {
top: "15px",
minHeight: "200px"
};
var errorMessage = "";
if (this.state.error) {
errorMessage = (
<Message negative>
<Message.Header>That didn't work...</Message.Header>
<p>{this.state.error}</p>
</Message>
)
}
return (
<Modal size='tiny' open={this.props.open} onClose={this.handleClose} centered={false}>
<Modal.Header>Select Role</Modal.Header>
<Modal.Content >
<Modal.Description>
<Form>
<Form.Field>
Add User Access to System: {this.props.shortName}
</Form.Field>
<Form.Field>
<Input label="User's email" placeholder='user@domain.com' name='email' onChange={this.handleInputChange} />
</Form.Field>
<Form.Field>
<Checkbox
radio
label='User'
name='role'
value={5}
checked={(this.state.role >= 0) && (this.state.role < 10)}
onChange={this.handleInputChange}
/>
</Form.Field>
<Form.Field>
<Checkbox
radio
label='Admin'
name='role'
value={15}
checked={(this.state.role >= 10) && (this.state.role < 20)}
onChange={this.handleInputChange}
/>
</Form.Field>
</Form>
{errorMessage}
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleClose} >
<Icon name='remove' /> Cancel
</Button>
<Button onClick={this.handleSubmit} >
<Icon name='plus' /> Add
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default AddPermissionModal;

View File

@@ -1,23 +0,0 @@
import { connect } from "react-redux"
import * as permissionActionCreators from "./permission-actions"
import { bindActionCreators } from 'redux'
import AddPermissionModal from "./AddPermissionModal"
//groups: state.group.items[props.match.params.shortName],
function mapStateToProps(state,props) {
return {
permissions: state.permission.items,
shortName: props.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
permissionActions: bindActionCreators(permissionActionCreators,dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(AddPermissionModal)

View File

@@ -1,76 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
// ----------------------------------------------------
class ListPermissions extends Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const permissions = this.props.permissions;
if (permissions) {
return (
<Table >
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
<Table.HeaderCell>Role</Table.HeaderCell>
<Table.HeaderCell></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{permissions.map((permission, i) => (
<Table.Row key={ "Group-" + i}>
<Table.Cell>{permission.firstName} {permission.lastName}</Table.Cell>
<Table.Cell>{permission.email}</Table.Cell>
<Table.Cell>{permission.role}</Table.Cell>
<Table.Cell><Icon
name="pencil alternate"
link={true}
onClick={e => this.props.updatePermission(permission._id)}
/>
<Icon
name="remove"
link={true}
onClick={e => this.props.deletePermission(permission)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
);
} else {
return (
<div/>
);
}
}
}
export default ListPermissions;

View File

@@ -1,134 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Checkbox,
Header,
Form,
Grid,
Modal,
Rail,
Segment,
List,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class UpdatePermissionModal extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleChange = this.handleChange.bind(this);
// Set initial state
this.state = {
newRole: false,
permission: false
};
}
componentDidMount() {
}
componentWillReceiveProps(nextProps) {
if (nextProps.permissions && nextProps.updatePermissionId) {
var updatePermission = nextProps.permissions.find(permission=> permission._id === nextProps.updatePermissionId);
if (updatePermission) {
this.setState({permission: updatePermission, newRole: updatePermission.role});
}
}
}
handleChange = (e, { name, value }) => this.setState({ newRole: value })
handleClose = () => this.props.onClose(false);
handleSubmit() {
if (this.props.updatePermissionId) {
const data = {permissionId: this.props.updatePermissionId, shortName: this.props.shortName, role: this.state.newRole }
this.props.permissionActions.update(data).then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
this.props.onClose(false);
} else {
this.props.onClose(true);
}
});
}
}
render() {
const railStyle = {
top: "15px",
minHeight: "200px"
};
var user = ""
if (this.state.permission) {
user = this.state.permission.email
}
return (
<Modal size='tiny' open={this.props.open} onClose={this.handleClose} centered={false}>
<Modal.Header>Select Role</Modal.Header>
<Modal.Content >
<Modal.Description>
<Form>
<Form.Field>
Set role for <b>{user}</b>:
</Form.Field>
<Form.Field>
<Checkbox
radio
label='User'
name='checkboxRadioGroup'
value={5}
checked={(this.state.newRole >= 0) && (this.state.newRole < 10)}
onChange={this.handleChange}
/>
</Form.Field>
<Form.Field>
<Checkbox
radio
label='Admin'
name='checkboxRadioGroup'
value={15}
checked={(this.state.newRole >= 10) && (this.state.newRole < 20)}
onChange={this.handleChange}
/>
</Form.Field>
</Form>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleSubmit} >
<Icon name='checkmark' /> Update
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default UpdatePermissionModal;

View File

@@ -1,23 +0,0 @@
import { connect } from "react-redux"
import * as permissionActionCreators from "./permission-actions"
import { bindActionCreators } from 'redux'
import UpdatePermissionModal from "./UpdatePermissionModal"
//groups: state.group.items[props.match.params.shortName],
function mapStateToProps(state,props) {
return {
permissions: state.permission.items,
shortName: props.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
permissionActions: bindActionCreators(permissionActionCreators,dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(UpdatePermissionModal)

View File

@@ -1,166 +0,0 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./permission-constants";
function beginAddPermission() {
return { type: types.ADD_PERMISSION };
}
function addPermissionSuccess(data) {
return { type: types.ADD_PERMISSION_SUCCESS, data };
}
function addPermissionError() {
return { type: types.ADD_PERMISSION_ERROR };
}
function beginDeletePermission() {
return { type: types.DELETE_PERMISSION };
}
function deletePermissionSuccess(data) {
return { type: types.DELETE_PERMISSION_SUCCESS, data };
}
function deletePermissionError() {
return { type: types.DELETE_PERMISSION_ERROR };
}
function beginUpdateRole() {
return { type: types.UPDATE_ROLE };
}
function updateRoleSuccess(data) {
return { type: types.UPDATE_ROLE_SUCCESS, data };
}
function updateRoleError() {
return { type: types.UPDATE_ROLE_ERROR };
}
// "Log In" action creators
function beginFetchPermission() {
return { type: types.FETCH_PERMISSION };
}
function fetchPermissionError(data) {
return { type: types.FETCH_ERROR_PERMISSION,
data };
}
function fetchPermissionSuccess(data) {
return { type: types.FETCH_SUCCESS_PERMISSION,
data };
}
function shouldFetch(state) {
if ((!state.permission.items) || (state.permission.items.length <1)) {
return true
} else if (state.permission.isWaiting) {
return false
} else {
return false
}
}
export function fetch(shortName) {
return (dispatch, getState) => {
if (shouldFetch(getState())) {
dispatch(beginFetchPermission());
return axios
.get("/permissions/"+shortName)
.then(response => {
if (response.data.success) {
dispatch(fetchPermissionSuccess(response.data.permissions));
} else {
dispatch(fetchPermissionError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}
export function update(data) {
return (dispatch, getState) => {
dispatch(beginUpdateRole());
var url = "/permissions/" + data.shortName + "/" + data.permissionId;
return axios.post( url, data)
.then(response => {
if (response.data.success) {
dispatch(updateRoleSuccess(response.data.permission));
} else {
dispatch(updateRoleError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
export function addPermission(data) {
return (dispatch, getState) => {
dispatch(beginAddPermission());
var url = "/permissions/" + data.shortName;
return axios.post( url, data)
.then(response => {
if (response.data.success) {
dispatch(addPermissionSuccess(response.data.permission));
} else {
dispatch(addPermissionError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}
export function deletePermission(data) {
return (dispatch, getState) => {
dispatch(beginDeletePermission());
var url = "/permissions/" + data.shortName + "/" + data.permissionId;
return axios.delete( url)
.then(response => {
if (response.data.success) {
dispatch(deletePermissionSuccess(data.permissionId));
} else {
dispatch(deletePermissionError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
}

View File

@@ -1,12 +0,0 @@
export const ADD_PERMISSION = "ADD_PERMISSION"
export const ADD_PERMISSION_ERROR = "ADD_PERMISSION_ERROR"
export const ADD_PERMISSION_SUCCESS = "ADD_PERMISSION_SUCCESS"
export const FETCH_PERMISSION = "FETCH_PERMISSION"
export const FETCH_ERROR_PERMISSION = "FETCH_ERROR_PERMISSION"
export const FETCH_SUCCESS_PERMISSION = "FETCH_SUCCESS_PERMISSION"
export const UPDATE_ROLE = "UPDATE_ROLE"
export const UPDATE_ROLE_ERROR = "UPDATE_ROLE_ERROR"
export const UPDATE_ROLE_SUCCESS = "UPDATE_ROLE SUCCESS"
export const DELETE_PERMISSION = "DELETE_PERMISSION"
export const DELETE_PERMISSION_ERROR = "DELETE_PERMISSION_ERROR"
export const DELETE_PERMISSION_SUCCESS = "DELETE_PERMISSION_SUCCESS"

View File

@@ -1,87 +0,0 @@
import {
ADD_PERMISSION,
ADD_PERMISSION_SUCCESS,
ADD_PERMISSION_ERROR,
DELETE_PERMISSION,
DELETE_PERMISSION_SUCCESS,
DELETE_PERMISSION_ERROR,
FETCH_PERMISSION,
FETCH_SUCCESS_PERMISSION,
FETCH_ERROR_PERMISSION,
UPDATE_ROLE,
UPDATE_ROLE_ERROR,
UPDATE_ROLE_SUCCESS
} from "./permission-constants";
function updateRoleInArray(array, updatedItem) {
const updatedItems = array.map(item => {
if(item._id !== updatedItem._id) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
item.role = updatedItem.role;
return item;
});
return updatedItems;
}
function deleteItemInArray(array, _id) {
const updatedItems = array.filter(function (item) {
return item._id !== _id;
});
return updatedItems;
}
const permission = (
state = {
isWaiting: false,
items: []
},
action
) => {
switch (action.type) {
case ADD_PERMISSION_SUCCESS:
return Object.assign({}, state, {
isWaiting: false,
items: [...state.items, action.data]
});
case DELETE_PERMISSION_SUCCESS:
return Object.assign({}, state, {
isWaiting: false,
items: deleteItemInArray(state.items, action.data)
});
case UPDATE_ROLE_SUCCESS:
return Object.assign({}, state, {
isWaiting: false,
items: updateRoleInArray(state.items, action.data)
});
case FETCH_SUCCESS_PERMISSION:
return Object.assign({}, state, {
isWaiting: false,
items: action.data
});
case DELETE_PERMISSION_ERROR:
case ADD_PERMISSION_ERROR:
case UPDATE_ROLE_ERROR:
case FETCH_ERROR_PERMISSION:
return Object.assign({}, state, {
isWaiting: false
});
case DELETE_PERMISSION:
case ADD_PERMISSION:
case UPDATE_ROLE:
case FETCH_PERMISSION:
return Object.assign({}, state, { isWaiting: true});
default:
return state;
}
};
export default permission;

View File

@@ -1,100 +0,0 @@
import React, { Component } from "react";
import { Group } from '@vx/group';
import { curveStep} from '@vx/curve';
import { LinePath } from '@vx/shape';
import { scaleLinear, scaleBand, scaleTime } from '@vx/scale';
import { timeParse, timeFormat } from 'd3-time-format';
import { AxisBottom, AxisLeft } from '@vx/axis';
import { Dimmer, Loader } from "semantic-ui-react";
class CallChart extends Component {
constructor(props) {
super(props);
}
render() {
var callData = this.props.data;
if (!callData) {
return (
<Loader size='large'>Loading</Loader>
)
}
var callTotals = [];
var errorTotals = [];
// Define the graph dimensions and margins
const width = this.props.width;
const height = 500;
const margin = { top: 20, bottom: 40, left: 40, right: 20 };
// Then we'll create some bounds
const xMax = width - margin.left - margin.right;
const yMax = height - margin.top - margin.bottom;
// accessors
const x = d => d.x;
const y = d => d.y;
// Compose together the scale and accessor functions to get point functions
const dotColor = n => {
var colores_g = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"];
return colores_g[n % colores_g.length];
}
const compose = (scale, accessor) => (data) => scale(accessor(data));
var xScale, yScale, xPoint, yPoint;
if (callData) {
// We'll use some mock data from `@vx/mock-data` for this.
callTotals = callData.callTotals;
errorTotals = callData.errorTotals;
// scales
xScale = scaleTime({
range: [0, xMax],
domain: [callData.minDate, callData.maxDate],
tickFormat: () => (val) => formatDate(val)
});
yScale = scaleLinear({
range: [yMax, 0],
domain: [0, callData.maxValue],
});
xPoint = compose(xScale, x);
yPoint = compose(yScale, y);
}
return (
<svg width={width} height={height}>
<Group left={margin.left} top={margin.top} >
<AxisBottom top={yMax} scale={xScale} numTicks={width > 520 ? 10 : 5} />
<AxisLeft scale={yScale} />
<LinePath
data={callTotals}
xScale={xScale}
yScale={yScale}
x={x}
y={y}
strokeWidth={2}
curve={curveStep}
/>
<LinePath
data={errorTotals}
xScale={xScale}
yScale={yScale}
x={x}
y={y}
strokeWidth={2}
stroke="red"
curve={curveStep}
/>
</Group>
</svg>
);
}
}
export default CallChart;

View File

@@ -1,82 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Divider,
Message,
Icon
} from "semantic-ui-react";
import SystemForm from "./SystemForm";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class CreateSystem extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
state = {
requestMessage: ""
}
handleSubmit(system) {
this.props
.createSystem(system)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
} else {
this.props.changeUrl("/list-systems")
}
});
}
render() {
return (
<div>
<Container text>
<Header as="h2">Join Us!</Header>
<p>
Record the your community's radio systems and share them! All it
takes is a spare computer and a cheap SDR. The software you need,
along with an explanation of how to set it up, is available{" "}
<a href="https://github.com/robotastic/trunk-recorder">here</a>.
</p>
<p>
After you have the Trunk Recorder software up and running, come
back here and sign-up for an account so you can share your
recordings
</p>
<p> - Luke</p>
<Divider />
<p></p>
</Container>
<Container text>
<Header as="h1">Create System</Header>
<SystemForm onSubmit={this.handleSubmit} requestMessage={this.state.requestMessage} screenName={this.props.user.screenName}/>
</Container>
</div>
);
}
}
export default CreateSystem;

View File

@@ -1,14 +0,0 @@
import { connect } from "react-redux"
import * as systemActions from "./system-actions"
import CreateSystem from "./CreateSystem"
function mapStateToProps(state) {
return {
user: state.user
}
}
export default connect(
mapStateToProps,
systemActions
)(CreateSystem)

View File

@@ -1,48 +0,0 @@
import React, { Component } from "react"
import SystemForm from "./SystemForm";
import {
Container,
Header
} from "semantic-ui-react";
class EditSystem extends Component {
constructor(props) {
super(props);
this.handleSystemSubmit = this.handleSystemSubmit.bind(this);
}
state = {
requestMessage: ""
}
componentDidMount() {
const { dispatch } = this.props
this.props.fetchSystems()
}
handleSystemSubmit(system) {
this.props
.updateSystem(system)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
} else {
this.props.changeUrl("/system/"+system.shortName)
}
});
}
render() {
return(
<Container text>
<Header as="h1">Update System</Header>
<SystemForm onSubmit={this.handleSystemSubmit} system={this.props.system} isEditing={true} requestMessage={this.state.requestMessage} screenName={this.props.user.screenName}/>
</Container>
)
}
}
export default EditSystem

View File

@@ -1,18 +0,0 @@
import { connect } from "react-redux"
import EditSystem from "./EditSystem"
import * as systemActions from "./system-actions"
const mapStateToProps = (state, props) => {
return {
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
user: state.user
}
}
export default connect(
mapStateToProps,
systemActions
)(EditSystem)

View File

@@ -1,133 +0,0 @@
import React, { Component } from "react";
import { Group } from '@vx/group';
import { GlyphCircle } from '@vx/glyph';
import { scaleLinear, scaleBand, scaleTime, scaleOrdinal } from '@vx/scale';
import { timeParse, timeFormat } from 'd3-time-format';
import { AxisBottom, AxisLeft } from '@vx/axis';
import { Dimmer, Loader } from "semantic-ui-react";
import {
LegendOrdinal
} from '@vx/legend';
class ErrorChart extends Component {
constructor(props) {
super(props);
}
render() {
var errorData = this.props.data;
if (!errorData) {
return (
<Loader size='large'>Loading</Loader>
)
}
var data = [];
const parseDate = timeParse("%Y%m%d");
const format = timeFormat("%b %d");
const formatDate = (date) => format(parseDate(date));
// Define the graph dimensions and margins
const width = this.props.width;
const height = 500;
const margin = { top: 20, bottom: 40, left: 40, right: 20 };
// Then we'll create some bounds
const xMax = width - margin.left - margin.right;
const yMax = height - margin.top - margin.bottom;
// accessors
const x = d => d.x;
const y = d => d.y;
// Compose together the scale and accessor functions to get point functions
const dotColor = n => {
var colores_g = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"];
return colores_g[n % colores_g.length];
}
const compose = (scale, accessor) => (data) => scale(accessor(data));
var zScale = scaleOrdinal({
domain: errorData.legend,
range: ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]
});
var xScale, yScale, xPoint, yPoint;
if (errorData) {
// We'll use some mock data from `@vx/mock-data` for this.
data = errorData.data;
// scales
xScale = scaleTime({
range: [0, xMax],
domain: [errorData.minDate, errorData.maxDate],
tickFormat: () => (val) => formatDate(val)
});
yScale = scaleLinear({
range: [yMax, 0],
domain: [0, errorData.maxValue],
});
zScale = scaleOrdinal({
domain: errorData.legend,
range: ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]
});
xPoint = compose(xScale, x);
yPoint = compose(yScale, y);
}
return (
<div style={{
width: 900
}}>
<svg width={width} height={height} style={{
float: "left"
}}>
<Group left={margin.left} top={margin.top} >
<AxisBottom top={yMax} scale={xScale} numTicks={width > 520 ? 10 : 5} />
<AxisLeft scale={yScale} />
{data.map((band, i) => {
return (
<Group key={`band-${i}`}>
{band.data.map((point, j) => {
return (
<GlyphCircle
className="dot"
key={`point-${point.x}-${j}`}
fill={dotColor(i)}
left={xScale(x(point))}
top={yScale(y(point))}
size={6.0}
/>
);
})}
</Group>
);
})}
</Group>
</svg>
<div
style={{
float: "right"
}}
>
<LegendOrdinal
scale={zScale}
labelMargin="0 15px 0 0"
onMouseOver={data => event => {
console.log(
`mouse over: ${data.text}`,
`index: ${data.index}`,
);
}}
/>
</div>
</div>
);
}
}
export default ErrorChart;

View File

@@ -1,70 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import SystemCard from "./SystemCard";
import {
Container,
Divider,
Header,
Card,
Form,
Grid,
Segment,
Input,
Icon,
Button,
Message
} from "semantic-ui-react";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
function SystemList(props) {
const systems = props.systems;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
// ----------------------------------------------------
class ListSystems extends Component {
constructor(props) {
super(props);
}
state = {
requestMessage: "",
}
componentDidMount() {
this.props.fetchSystems();
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const systems = this.props.system.items;
return (
<div>
<Container>
<Header as="h1">Radio Systems</Header>
<Button onClick={(e) => this.props.changeUrl("/create-system")}><Icon name='plus' />Add System</Button>
<Divider />
<Card.Group itemsPerRow={4}>
{systems.map((system) =>
<SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
)}
</Card.Group>
</Container>
</div>
);
}
}
export default ListSystems;

View File

@@ -1,16 +0,0 @@
import { connect } from "react-redux"
import * as systemActions from "./system-actions"
import ListSystems from "./ListSystems"
function mapStateToProps(state) {
return {
system: state.system
}
}
export default connect(
mapStateToProps,
systemActions
)(ListSystems)

View File

@@ -1,72 +0,0 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Step,
Icon
} from "semantic-ui-react";
import SystemForm from "./SystemForm";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class PreCreateSystem extends Component {
constructor(props) {
super(props);
}
state = {
requestMessage: ""
}
render() {
return (
<div>
<Step.Group attached='top' widths={3}>
<Step>
<Icon name='wifi' />
<Step.Content>
<Step.Title>Trunk Recorder</Step.Title>
<Step.Description>Choose your shipping options</Step.Description>
</Step.Content>
</Step>
<Step active>
<Icon name='payment' />
<Step.Content>
<Step.Title>Billing</Step.Title>
<Step.Description>Enter billing information</Step.Description>
</Step.Content>
</Step>
<Step disabled>
<Icon name='info' />
<Step.Content>
<Step.Title>Confirm Order</Step.Title>
</Step.Content>
</Step>
</Step.Group>
<Segment attached>
<Image src='https://react.semantic-ui.com/images/wireframe/paragraph.png' />
</Segment>
</div>
);
}
}
export default PreCreateSystem;

View File

@@ -1,27 +0,0 @@
import React, { Component } from "react"
import CallChart from "./CallChart"
import { ParentSize } from "@vx/responsive";
class ResponsiveCallChart extends Component {
constructor(props) {
super(props);
}
render() {
return(
<ParentSize className="graph-container">
{({ width: w, height: h }) => {
return (
<CallChart
width={w}
height={h}
data={this.props.data}
/>
);
}}
</ParentSize>
)
}
}
export default ResponsiveCallChart

View File

@@ -1,27 +0,0 @@
import React, { Component } from "react"
import ErrorChart from "./ErrorChart"
import { ParentSize } from "@vx/responsive";
class ResponsiveErrorChart extends Component {
constructor(props) {
super(props);
}
render() {
return(
<ParentSize className="graph-container">
{({ width: w, height: h }) => {
return (
<ErrorChart
width={w}
height={h}
data={this.props.data}
/>
);
}}
</ParentSize>
)
}
}
export default ResponsiveErrorChart

View File

@@ -1,679 +0,0 @@
import React, {Component} from "react";
import {Link} from "react-router-dom";
import ListTalkgroups from "../Talkgroups/ListTalkgroups.js";
import MessageBox from "../components/pure/Message.js";
import ListGroups from "../Group/ListGroups.js";
import GroupModal from "../Group/GroupModalContainer.js";
import UpdatePermissionModal from "../Permission/UpdatePermissionModalContainer.js";
import AddPermissionModal from "../Permission/AddPermissionModalContainer.js";
import ErrorChart from "./ResponsiveErrorChart"
import CallChart from "./ResponsiveCallChart"
import {
Button,
Confirm,
Icon,
Form,
TextArea,
Header,
Container,
Message,
List,
Segment,
Tab,
Divider,
Label
} from "semantic-ui-react";
function planName(planType) {
switch (planType) {
case freePlanValue:
return "Free";
case proPlanValue:
return "Pro";
}
}
class System extends Component {
constructor(props) {
super(props);
this.handleUpload = this.handleUpload.bind(this);
this.reorderGroup = this.reorderGroup.bind(this);
this.deleteGroup = this.deleteGroup.bind(this);
this.editGroup = this.editGroup.bind(this);
this.saveGroupOrder = this.saveGroupOrder.bind(this);
this.showSystemDeleteConfirm = this.showSystemDeleteConfirm.bind(this);
this.showPermissionDeleteConfirm = this.showPermissionDeleteConfirm.bind(
this
);
this.updatePermission = this.updatePermission.bind(this);
this.handlePermissionDeleteConfirm = this.handlePermissionDeleteConfirm.bind(
this
);
this.handlePermissionDeleteCancel = this.handlePermissionDeleteCancel.bind(
this
);
this.handleSystemDeleteConfirm = this.handleSystemDeleteConfirm.bind(this);
this.handleSystemDeleteCancel = this.handleSystemDeleteCancel.bind(this);
this.handleUpgradePlan = this.handleUpgradePlan.bind(this);
this.handleUpdatePermissionClose = this.handleUpdatePermissionClose.bind(
this
);
this.handleGroupToggle = this.handleGroupToggle.bind(this);
this.handleGroupClose = this.handleGroupClose.bind(this);
this.handleAddPermissionToggle = this.handleAddPermissionToggle.bind(this);
this.handleAddPermissionClose = this.handleAddPermissionClose.bind(this);
this.processErrors = this.processErrors.bind(this);
this.processStatistics = this.processStatistics.bind(this);
this.state = {
openSystemDeleteConfirm: false,
openPermissionDeleteConfirm: false,
openMessage: false,
requestMessage: "",
groupVisible: false,
editGroupId: false,
selectedPermission: false,
addPermissionVisible: false,
updatePermissionVisible: false,
updatePermissionId: false
};
}
processErrors(errors) {
var allData = [];
var num = 0;
const now = new Date();
var maxDate = now;
var minDate = now;
var minValue = 0;
var maxValue = 0;
var legend = [];
for (var j = 0; j < errors.length; j++) {
var data = [];
for (var i = 0; i < errors[j].values.length; i++) {
var time = new Date(errors[j].values[i].time);
if (errors[j].values[i].errors > maxValue)
maxValue = errors[j].values[i].errors;
if (time < minDate) minDate = time;
var value = {
id: num++,
y: errors[j].values[i].errors,
x: time
};
data.push(value);
}
const id = Math.floor(errors[j]._id / 1000) / 1000 + "MHz";
legend.push(id);
var obj = {
id: id,
data: data
};
allData.push(obj);
}
const errorData = {
data: allData,
minDate: minDate,
maxDate: maxDate,
minValue: minValue,
maxValue: maxValue,
legend: legend
};
this.setState({ errorData: errorData });
}
processStatistics(statistic) {
var callTotals = [];
var errorTotals = [];
const now = new Date();
var maxDate = now;
var minDate = now;
var minValue = 0;
var maxValue = 0;
var MS_PER_MINUTE = 60000;
for (var j = 0; j < statistic.callTotals.length; j++) {
var spotsBack = statistic.callTotals.length - j;
var time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
if (time < minDate) minDate = time;
if (statistic.callTotals[j] > maxValue)
maxValue = statistic.callTotals[j];
callTotals.push({ y: statistic.callTotals[j], x: time });
}
for (var j = 0; j < statistic.errorTotals.length; j++) {
var spotsBack = statistic.errorTotals.length - j;
var time = new Date(now - spotsBack * 15 * MS_PER_MINUTE);
if (time < minDate) minDate = time;
if (statistic.errorTotals[j] > maxValue)
maxValue = statistic.errorTotals[j];
errorTotals.push({ x: time, y: statistic.errorTotals[j] });
}
var callData = {
minDate: minDate,
maxDate: maxDate,
minValue: minValue,
maxValue: maxValue,
callTotals: callTotals,
errorTotals: errorTotals
};
this.setState({ callData: callData });
}
componentDidMount() {
this.props.systemActions.fetchSystems();
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
this.props.groupActions.fetchGroups(this.props.shortName);
this.props.errorActions.fetch(this.props.shortName);
this.props.permissionActions.fetch(this.props.shortName);
if (this.props.statistic) {
this.processStatistics(this.props.statistic);
}
if (this.props.errors) {
this.processErrors(this.props.errors);
}
}
componentWillReceiveProps(nextProp) {
if (!this.props.errors && nextProp.errors) {
this.processErrors(nextProp.errors);
}
if (!this.props.statistic && nextProp.statistic) {
this.processStatistics(nextProp.statistic);
}
}
updatePermission(permissionId) {
this.setState({
updatePermissionId: permissionId,
updatePermissionVisible: !this.state.updatePermissionVisible
});
}
editGroup(groupId) {
this.setState({
editGroupId: groupId,
groupVisible: !this.state.groupVisible
});
}
deleteGroup(groupId) {
this.props.groupActions.deleteGroup(this.props.shortName, groupId);
}
reorderGroup(oldIndex, newIndex) {
this.props.groupActions.reorderGroup(
this.props.shortName,
oldIndex,
newIndex
);
}
handleUpdatePermissionClose(didUpdate) {
this.setState({
updatePermissionVisible: !this.state.updatePermissionVisible,
updatePermissionId: false
});
}
handleAddPermissionClose(didUpdate) {
this.setState({
addPermissionVisible: !this.state.addPermissionVisible
});
}
handleAddPermissionToggle = () =>
this.setState({
addPermissionVisible: !this.state.addPermissionVisible
});
handleGroupClose(didUpdate) {
this.setState({
groupVisible: !this.state.groupVisible,
editGroupId: false
});
}
handleGroupToggle = () => this.setState({
editGroupId: false,
groupVisible: !this.state.groupVisible
})
handleExport() {
this.props.talkgroupActions
.exportTalkgroups(this.props.system.shortName)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({ requestMessage });
}
});
}
saveGroupOrder() {
const order = this.props.groups.map(x => x._id);
const data = {
groupOrder: JSON.stringify(order)
};
this.props.groupActions
.saveGroupOrder(this.props.system.shortName, data)
.then(requestMessage => {
if (requestMessage) {
}
});
}
handleUpgradePlan() {
window.location = account_server + "/update-plans";
}
handleUpload(file) {
this.props.talkgroupActions
.importTalkgroups(this.props.system.shortName, file)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({ requestMessage });
}
});
}
// https://github.com/Semantic-Org/Semantic-UI-React/issues/2103
showPermissionDeleteConfirm = permission =>
this.setState({
openPermissionDeleteConfirm: true,
selectedPermission: permission
});
showSystemDeleteConfirm = () =>
this.setState({ openSystemDeleteConfirm: true });
handlePermissionDeleteConfirm = permissionId => {
this.setState({ openPermissionDeleteConfirm: false });
this.props.permissionActions
.deletePermission({
shortName: this.props.system.shortName,
permissionId: this.state.selectedPermission._id
})
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during login
this.setState({ requestMessage: requestMessage, openMessage: true });
}
});
};
handleSystemDeleteConfirm = () => {
this.setState({ openSystemDeleteConfirm: false });
this.props.systemActions
.deleteSystem(this.props.system.shortName)
.then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during login
this.setState({ requestMessage: requestMessage, openMessage: true });
}
});
};
handleSystemDeleteCancel = () =>
this.setState({ openSystemDeleteConfirm: false });
handlePermissionDeleteCancel = () =>
this.setState({ openPermissionDeleteConfirm: false });
render() {
const system = this.props.system;
let fileInput = null;
var location = "";
var example = "";
var plan = "";
var groupRender = () => {};
if (system) {
if (this.props.talkgroups && this.props.talkgroups.length) {
groupRender = () => (
<div>
<Button onClick={this.handleGroupToggle}>Create Group</Button>
<Button onClick={this.saveGroupOrder} disabled={!this.props.orderChange}> Save Order</Button>
<ListGroups
groups={this.props.groups}
reorderGroup={this.reorderGroup}
deleteGroup={this.deleteGroup}
editGroup={this.editGroup}
/>
</div>
);
} else {
groupRender = () => (
<Container text>
<Message
icon="upload"
header="Import Talkgroups"
content="Before you can create groups of Talkgroups, you need to import some talkgroups."
/>
</Container>
);
}
switch (system.planType) {
case freePlanValue:
const tempPlan = planName(system.planType) + " plan";
plan = (
<div>
{tempPlan}
<Label as="a" onClick={this.handleUpgradePlan}>
{" "}
<Icon name="plus" />
Upgrade Plan
</Label>
</div>
);
break;
case proPlanValue:
plan = planName(system.planType) + " plan";
break;
}
example =
'{\n\t"systems": [\n\t\t{\n\t\t\t"shortName": "' +
system.shortName +
'",\n\t\t\t"apiKey": "' +
system.key +
'"\n\t\t}\n\t],\n\t"uploadServer": "' +
backend_server +
'"\n}';
switch (system.systemType) {
case "state":
location = system.state;
break;
case "city":
location = system.city + ", " + system.state;
break;
case "county":
location = system.county + ", " + system.state;
break;
case "international":
location = system.country;
break;
}
}
const panes = [
{
menuItem: "Overview",
render: () => (
<div>
<Container text>
<Segment.Group>
<Segment>
<Header size="small">Location</Header>
{location}
</Segment>
<Segment>
<Header size="small">Short Name</Header>
{this.props.system.shortName}
</Segment>
<Segment>
<Header size="small">Description</Header>
{this.props.system.description}
</Segment>
<Segment clearing={true}>
<Button
color="red"
onClick={this.showSystemDeleteConfirm}
disabled={this.props.systemisWaiting}
content="Delete"
floated="right"
/>
<Button
as={Link}
to={"/update-system/" + this.props.system.shortName}
content="Update"
disabled={this.props.systemisWaiting}
floated="left"
/>
</Segment>
</Segment.Group>
</Container>
</div>
)
},
{
menuItem: "Config",
render: () => (
<div>
<Container text>
<Header as="h2">Configure Trunk Recorder</Header>
<p>
Below are the parameters you will need to configure Trunk
Recorder. The example below shows how they should be added to
the config.json file.
</p>
<Segment.Group>
<Segment>
<Header size="small">API Key</Header>
{this.props.system.key}
</Segment>
<Segment>
<Header size="small">Short Name</Header>
{this.props.system.shortName}
</Segment>
<Segment>
<Header size="small">Upload Server</Header>
{backend_server}
</Segment>
<Segment>
<Header size="small">Example</Header>
<Form>
<TextArea rows="10" readOnly="readOnly" value={example} />
</Form>
</Segment>
</Segment.Group>
</Container>
</div>
)
},
{
menuItem: "Talkgroups",
render: () => (
<div>
<Container text>
<p>
<em>
This is the same .csv talkgroup file from Trunk Recorder...
except the Hex column has been deleted
</em>
</p>
<p>The columns for the file are:</p>
<List ordered celled>
<List.Item>
<List.Content>
<List.Header>Number</List.Header>
<List.Description>The talkgroup number</List.Description>
</List.Content>
</List.Item>
<List.Item>
<List.Content>
<List.Header>Mode</List.Header>
<List.Description>
The mode for the talkgroup, it can be: 'A' = analog, 'D' =
P25 Digital, 'E' = Encrypted
</List.Description>
</List.Content>
</List.Item>
<List.Item>
<List.Content>
<List.Header>Alpha</List.Header>
<List.Description>
Up to 12 charecter description of the talkgroup
</List.Description>
</List.Content>
</List.Item>
<List.Item>
<List.Content>
<List.Header>Description</List.Header>
<List.Description>
The full title for the talkgroup
</List.Description>
</List.Content>
</List.Item>
<List.Item>
<List.Content>
<List.Header>Tag</List.Header>
<List.Description>
The type of talkgroup, eg: Dispatch, Fireground
</List.Description>
</List.Content>
</List.Item>
<List.Item>
<List.Content>
<List.Header>Group</List.Header>
<List.Description>
The department that uses this talkgroup
</List.Description>
</List.Content>
</List.Item>
<List.Item>
<List.Content>
<List.Header>Priority</List.Header>
<List.Description>
The priority for recording this talkgroup, 100 or over
will be skipped
</List.Description>
</List.Content>
</List.Item>
</List>
</Container>
<Divider />
<span>
<label htmlFor="upload-button" className="ui icon button">
<i className="upload icon" />
Import Talkgroups
</label>
<input
type="file"
id="upload-button"
style={{
display: "none"
}}
onChange={() => {
this.handleUpload(fileInput.files[0]);
}}
ref={input => {
fileInput = input;
}}
/>
</span>
<a href={"/talkgroups/" + this.props.system.shortName + "/export"}>
<Button>
<i className="upload icon" />
Export Talkgroups
</Button>
</a>
<ListTalkgroups talkgroups={this.props.talkgroups} />
</div>
)
},
{
menuItem: "Groups",
render: groupRender
},
/*{
menuItem: "Users",
render: () => (<Tab.Pane>
<Button onClick={this.handleAddPermissionToggle} >
Add User
</Button>
<ListPermissions permissions={this.props.permissions} updatePermission={this.updatePermission} deletePermission={this.showPermissionDeleteConfirm}/>
</Tab.Pane>)
},*/ {
menuItem: "Stats",
render: () => (
<div>
<Divider horizontal>
<Icon name="microphone" />
24-Hour Call History
</Divider>
<CallChart data={this.state.callData} />
<Divider horizontal>
<Icon name="microphone" />
Hourly Vocoder Error Percentage
</Divider>
<ErrorChart data={this.state.errorData} />
</div>
)
}
];
return (
<div>
<AddPermissionModal
shortName={this.props.shortName}
open={this.state.addPermissionVisible}
onClose={this.handleAddPermissionClose}
/>
<GroupModal
shortName={this.props.shortName}
editGroupId={this.state.editGroupId}
open={this.state.groupVisible}
onClose={this.handleGroupClose}
/>
<UpdatePermissionModal
shortName={this.props.shortName}
updatePermissionId={this.state.updatePermissionId}
open={this.state.updatePermissionVisible}
onClose={this.handleUpdatePermissionClose}
/>
{system ? (
<Container>
<MessageBox
open={this.state.openMessage}
message={this.state.requestMessage}
title="Delete Failed"
onClose={() => this.setState({ openMessage: false })}
/>
<Confirm
open={this.state.openSystemDeleteConfirm}
header="Are you sure you want to?"
content={"Delete system - " + this.props.system.shortName}
onCancel={this.handleSystemDeleteCancel}
onConfirm={this.handleSystemDeleteConfirm}
/>
<Confirm
open={this.state.openPermissionDeleteConfirm}
header="Are you sure you want to?"
content={
"Remove access for: " + this.state.selectedPermission.email
}
onCancel={this.handlePermissionDeleteCancel}
onConfirm={this.handlePermissionDeleteConfirm}
/>
<Header as="h1">
<Header.Content>
{this.props.system.name}
{/* <Header.Subheader>{plan}</Header.Subheader> */}
</Header.Content>
</Header>
<Tab
menu={{
secondary: true,
pointing: true
}}
panes={panes}
defaultActiveIndex={this.props.activeTab}
/>
</Container>
) : (
<div />
)}
</div>
);
}
}
export default System;

View File

@@ -1,74 +0,0 @@
import React, { Component } from "react";
import {
Card,
Icon,
Label,
Header
} from "semantic-ui-react";
function planName(planType) {
switch(planType) {
case freePlanValue:
return "Free";
case proPlanValue:
return "Pro";
}
}
class SystemCard extends Component {
constructor(props) {
super(props);
}
render() {
var location = "";
const system = this.props.system;
var plan=null;
if (system) {
if (system.planType) {
plan = planName(system.planType);
}
switch (system.systemType) {
case "state":
location = system.state;
break;
case "city":
location = system.city + ", " + system.state;
break;
case "county":
location = system.county + ", " + system.state;
break;
case "international":
location = system.country;
break;
}
}
return (
<Card link onClick={this.props.onClick}>
<Card.Content>
<Header size='medium'>
<Header.Content>
{system.name}
<Header.Subheader>{system.screenName}</Header.Subheader>
</Header.Content>
</Header>{plan&&<Label color='orange' ribbon='right'>{plan}</Label>}
</Card.Content><Card.Content>
<Card.Description>{system.description}</Card.Description>
</Card.Content>
<Card.Content extra>
<Icon name='marker' />
{location}
</Card.Content>
</Card>
);
}
}
export default SystemCard;

Some files were not shown because too many files have changed in this diff Show More