First Commit

This commit is contained in:
Luke Berndt
2020-06-09 22:11:11 -04:00
parent 8f49aa463c
commit 3b0d3f290f
551 changed files with 193130 additions and 2 deletions

View File

@@ -0,0 +1,258 @@
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 ? "Create Group" : "Edit 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

@@ -0,0 +1,26 @@
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

@@ -0,0 +1,82 @@
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

@@ -0,0 +1,244 @@
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

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,128 @@
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

@@ -0,0 +1,150 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,76 @@
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

@@ -0,0 +1,134 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,166 @@
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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,87 @@
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

@@ -0,0 +1,100 @@
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

@@ -0,0 +1,82 @@
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

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,48 @@
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

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,133 @@
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

@@ -0,0 +1,70 @@
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

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,72 @@
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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,684 @@
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";
}
}
var backend_server =
process.env["BACKEND_SERVER"] != null
? process.env["BACKEND_SERVER"]
: "https://api.openmhz.com";
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

@@ -0,0 +1,74 @@
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;

View File

@@ -0,0 +1,40 @@
import { connect } from "react-redux"
import * as systemActionCreators from "./system-actions"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import * as groupActionCreators from "../Group/group-actions"
import * as permissionActionCreators from "../Permission/permission-actions"
import * as errorActionCreators from "./error-actions"
import { bindActionCreators } from 'redux'
import System from "./System"
function mapStateToProps(state,props) {
return {
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
permissions: state.permission.items,
statistic: state.statistic[props.match.params.shortName],
systemIsWaiting: state.system.isWaiting,
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
talkgroups: state.talkgroup.items[props.match.params.shortName],
orderChange: state.group.orderChange,
groups: state.group.items[props.match.params.shortName],
errors: state.error.items[props.match.params.shortName],
shortName: props.match.params.shortName,
activeTab: (props.match.params.tab ? props.match.params.tab : 0)
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
systemActions: bindActionCreators(systemActionCreators, dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch),
errorActions: bindActionCreators(errorActionCreators, dispatch),
permissionActions: bindActionCreators(permissionActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(System)

View File

@@ -0,0 +1,428 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Dropdown,
Divider,
TextArea,
Form,
Grid,
Segment,
Input,
Button,
Message,
Checkbox,
Icon
} from "semantic-ui-react";
// ----------------------------------------------------
const checkInputMessagestyle = {
color: "red"
};
// ----------------------------------------------------
class SystemForm extends Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.checkInputs = this.checkInputs.bind(this);
this.render = this.render.bind(this);
this.state = {
checkInputMessages: [],
name: "",
shortName: "",
description: "",
systemType: "state",
state: "",
city: "",
county: "",
country: "",
showScreenName: false,
nameError: false,
shortNameError: false,
descriptionError: false,
emailError: false,
systemTypeError: false,
stateError: false,
cityErorr: false,
countyError: false,
countryError: false,
isEditing: this.props.isEditing
};
if (this.props.isEditing && this.props.system)
{
this.state.name = this.props.system.name;
this.state.shortName = this.props.system.shortName;
this.state.description = this.props.system.description;
this.state.systemType = this.props.system.systemType;
this.state.state = this.props.system.state;
this.state.county = this.props.system.county;
this.state.country = this.props.system.country;
this.state.showScreenName = this.props.system.showScreenName;
}
}
componentWillReceiveProps(nextProps) {
if (this.props.isEditing && nextProps.system) {
this.setState({
name: nextProps.system.name,
shortName: nextProps.system.shortName,
description: nextProps.system.description,
systemType: nextProps.system.systemType,
state: nextProps.system.state,
county: nextProps.system.county,
country: nextProps.system.country,
showScreenName: nextProps.system.showScreenName,
});
}
}
checkInputs() {
let error = false;
var checkInputMessages = [];
if (this.state.name === "") {
this.setState({ nameError: true });
checkInputMessages.push("System Name is required");
error = true;
} else {
this.setState({ nameError: false });
}
if (this.state.shortName.length == 0 || this.state.shortName.length > 10) {
this.setState({ shortNameError: true });
checkInputMessages.push(
"Short Name for the system has to be 4-10 charecters"
);
error = true;
} else {
this.setState({ shortNameError: false });
}
if (this.state.description === "") {
this.setState({ descriptionError: true });
checkInputMessages.push("System description is required");
error = true;
} else {
this.setState({ descriptionError: false });
}
if (
this.state.systemType === "state" ||
this.state.systemType === "city" ||
this.state.systemType === "county"
) {
if (this.state.state === "") {
this.setState({ stateError: true });
checkInputMessages.push("Select a state");
error = true;
} else {
this.setState({ stateError: false });
}
}
if (this.state.systemType === "city") {
if (this.state.city === "") {
this.setState({ cityError: true });
checkInputMessages.push("Enter a city");
error = true;
} else {
this.setState({ cityError: false });
}
}
if (this.state.systemType === "county") {
if (this.state.county === "") {
this.setState({ countyError: true });
checkInputMessages.push("Enter a city");
error = true;
} else {
this.setState({ countyError: false });
}
}
if (this.state.systemType === "international") {
if (this.state.country === "") {
this.setState({ countryError: true });
checkInputMessages.push("Enter a city");
error = true;
} else {
this.setState({ countryError: false });
}
}
this.setState({ checkInputMessages: checkInputMessages });
return error;
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleCheckboxChange() {
this.setState({showScreenName: !this.state.showScreenName});
}
handleSubmit(event) {
event.preventDefault();
let inputError = this.checkInputs();
if (!inputError) {
const system = (({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
showScreenName
}) => ({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
showScreenName
}))(this.state);
this.props.onSubmit(system);
}
}
render() {
var requestMessage = "";
var floatStyle = {
overflow: "auto"
};
const { isEditing } = this.props;
const stateOptions = [
{ value: "AL", text: "Alabama" },
{ value: "AK", text: "Alaska" },
{ value: "AZ", text: "Arizona" },
{ value: "AR", text: "Arkansas" },
{ value: "CA", text: "California" },
{ value: "CO", text: "Colorado" },
{ value: "CT", text: "Connecticut" },
{ value: "DE", text: "Delaware" },
{ value: "DC", text: "District Of Columbia" },
{ value: "FL", text: "Florida" },
{ value: "GA", text: "Georgia" },
{ value: "HI", text: "Hawaii" },
{ value: "ID", text: "Idaho" },
{ value: "IL", text: "Illinois" },
{ value: "IN", text: "Indiana" },
{ value: "IA", text: "Iowa" },
{ value: "KS", text: "Kansas" },
{ value: "KY", text: "Kentucky" },
{ value: "LA", text: "Louisiana" },
{ value: "ME", text: "Maine" },
{ value: "MD", text: "Maryland" },
{ value: "MA", text: "Massachusetts" },
{ value: "MI", text: "Michigan" },
{ value: "MN", text: "Minnesota" },
{ value: "MS", text: "Mississippi" },
{ value: "MO", text: "Missouri" },
{ value: "MT", text: "Montana" },
{ value: "NE", text: "Nebraska" },
{ value: "NV", text: "Nevada" },
{ value: "NH", text: "New Hampshire" },
{ value: "NJ", text: "New Jersey" },
{ value: "NM", text: "New Mexico" },
{ value: "NY", text: "New York" },
{ value: "NC", text: "North Carolina" },
{ value: "ND", text: "North Dakota" },
{ value: "OH", text: "Ohio" },
{ value: "OK", text: "Oklahoma" },
{ value: "OR", text: "Oregon" },
{ value: "PA", text: "Pennsylvania" },
{ value: "RI", text: "Rhode Island" },
{ value: "SC", text: "South Carolina" },
{ value: "SD", text: "South Dakota" },
{ value: "TN", text: "Tennessee" },
{ value: "TX", text: "Texas" },
{ value: "UT", text: "Utah" },
{ value: "VT", text: "Vermont" },
{ value: "VA", text: "Virginia" },
{ value: "WA", text: "Washington" },
{ value: "WV", text: "West Virginia" },
{ value: "WI", text: "Wisconsin" },
{ value: "WY", text: "Wyoming" }
];
if (
this.state.checkInputMessages.length ||
this.props.requestMessage.length
) {
var counter = 0;
requestMessage = (
<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}>
<Form.Group widths="equal">
<Form.Field>
<Form.Input
type="text"
name="name"
onChange={this.handleInputChange}
error={this.state.nameError}
value={this.state.name}
label="System name"
placeholder="System Name..."
/>
</Form.Field>
<Form.Field>
<Form.Input
type="text"
name="shortName"
onChange={this.handleInputChange}
error={this.state.shortNameError}
value={this.state.shortName}
disabled={isEditing}
label="System Short Name (4-10 characters)"
placeholder="Short Name..."
/>
</Form.Field>
</Form.Group>
<Divider horizontal />
<Form.Field>
<p>Your Screen Name: {this.props.screenName}</p>
<Checkbox
onChange={this.handleCheckboxChange}
checked={this.state.showScreenName}
label="Show in System Listing" />
</Form.Field>
<Divider horizontal />
<Form.TextArea
label="Description"
name="description"
onChange={this.handleInputChange}
error={this.state.descriptionError}
value={this.state.description}
rows={2}
placeholder="Description of system..."
/>
<Divider horizontal><Icon name="marker" />Location</Divider>
<Form.Group inline>
<label>System Type</label>
<Form.Radio
label="State"
name="systemType"
value="state"
checked={this.state.systemType === "state"}
onChange={this.handleInputChange}
/>
<Form.Radio
label="County"
name="systemType"
value="county"
checked={this.state.systemType === "county"}
onChange={this.handleInputChange}
/>
<Form.Radio
label="City"
name="systemType"
value="city"
checked={this.state.systemType === "city"}
onChange={this.handleInputChange}
/>
<Form.Radio
label="International"
name="systemType"
value="international"
checked={this.state.systemType === "international"}
onChange={this.handleInputChange}
/>
</Form.Group>
<Form.Group widths="equal">
{this.state.systemType === "city" && (
<Form.Field>
<Form.Input
type="text"
name="city"
onChange={this.handleInputChange}
error={this.state.cityError}
value={this.state.city}
label="City"
placeholder="City..."
/>
</Form.Field>
)}
{this.state.systemType === "county" && (
<Form.Field>
<Form.Input
type="text"
name="county"
onChange={this.handleInputChange}
error={this.state.countyError}
value={this.state.county}
label="County"
placeholder="County..."
/>
</Form.Field>
)}
{this.state.systemType === "international" ? (
<Form.Field>
<Form.Input
type="text"
name="country"
onChange={this.handleInputChange}
error={this.state.countryError}
value={this.state.country}
label="Country"
placeholder="Country..."
/>
</Form.Field>
) : (
<Form.Field>
<Form.Dropdown
name="state"
onChange={this.handleInputChange}
error={this.state.stateError}
value={this.state.state}
options={stateOptions}
label="State"
placeholder="Select State..."
/>
</Form.Field>
)}
</Form.Group>
<div style={floatStyle}>
<Button type="submit" size="large" floated="right" value="Login">
{isEditing != true ? "Create" : "Update"}
</Button>
</div>
</Form>
{requestMessage}
</div>
);
}
}
export default SystemForm;

View File

@@ -0,0 +1,63 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./error-constants";
import { push } from 'connected-react-router';
function beginFetch() {
return { type: types.FETCH };
}
function fetchSuccess(data) {
return { type: types.FETCH_SUCCESS,
data };
}
function fetchError(data) {
return { type: types.FETCH_ERROR,
data };
}
function makeUserRequest(method, data, api) {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
export function fetch(shortName) {
return (dispatch, getState) => {
dispatch(beginFetch());
return axios
.get(backend_server + "/" + shortName + "/errors")
.then(response => {
if (response.data) {
var data ={
shortName: shortName,
errors: response.data
}
dispatch(fetchSuccess(data));
} else {
dispatch(fetchError());
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

@@ -0,0 +1,3 @@
export const FETCH = "FETCH"
export const FETCH_SUCCESS = "FETCH_SUCCESS"
export const FETCH_ERROR = "FETCH_ERROR"

View File

@@ -0,0 +1,42 @@
import {
FETCH,
FETCH_SUCCESS,
FETCH_ERROR
} from "./error-constants";
const error = (
state = {
isWaiting: false,
items: {}
},
action
) => {
switch (action.type) {
case FETCH:
return Object.assign({}, state, { isWaiting: true});
case FETCH_SUCCESS:
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: action.data.errors
}
});
case FETCH_ERROR:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default error;

View File

@@ -0,0 +1,20 @@
const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
const statistic = (
state = {},
action
) => {
switch (action.type) {
case FETCH_SUCCESS_STATISTIC:
return Object.assign({}, state, {...action.data});
default:
return state;
}
};
export default statistic;

View File

@@ -0,0 +1,233 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./system-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginCreateSystem() {
return { type: types.CREATE_SYSTEM };
}
function createSystemSuccess(data) {
return {
type: types.CREATE_SUCCESS_SYSTEM,
data
};
}
function createSystemError(data) {
return {
type: types.CREATE_ERROR_SYSTEM,
data
};
}
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 beginDeleteSystem() {
return { type: types.DELETE_SYSTEM };
}
function deleteSystemSuccess(data) {
return { type: types.DELETE_SUCCESS_SYSTEM,
data };
}
function deleteSystemError() {
return { type: types.DELETE_ERROR_SYSTEM };
}
function beginFetchSystem() {
return { type: types.FETCH_SYSTEM };
}
function fetchSystemSuccess(data) {
return { type: types.FETCH_SUCCESS_SYSTEM,
data };
}
function fetchStatisticSuccess(data) {
return { type: types.FETCH_SUCCESS_STATISTIC,
data };
}
function fetchSystemError(data) {
return { type: types.FETCH_ERROR_SYSTEM,
data };
}
function beginFetchErrors() {
return { type: types.FETCH_ERROR };
}
function fetchErrorsSuccess(data) {
return { type: types.FETCH_SUCCESS_ERROR,
data };
}
function fetchErrorsError(data) {
return { type: types.FETCH_ERROR_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));
};
}
// 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, "/systems/" + data.shortName)
.then(response => {
if (response.data.success) {
const user = response.data.user
dispatch(updateSystemSuccess(response.data.system));
} else {
dispatch(updateSystemError());
return response.data.message;
}
})
.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 fetchErrors(shortName) {
return (dispatch, getState) => {
dispatch(beginFetchError());
return axios
.get(backend_server + "/" + shortName + "/systems")
.then(response => {
if (response.data) {
dispatch(fetchErrorsSuccess(shortName, response.data));
} else {
dispatch(fetchErrorsError());
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 fetchSystems() {
return (dispatch, getState) => {
if (shouldFetchSystems(getState())) {
dispatch(beginFetchSystem());
return axios
.get("/systems")
.then(response => {
if (response.data.success) {
dispatch(fetchSystemSuccess(response.data.systems));
dispatch(fetchStatisticSuccess(response.data.stats));
} 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);
}
});
};
};
}
export function deleteSystem(shortName) {
return dispatch => {
dispatch(beginDeleteSystem());
return makeUserRequest("delete", null, "/systems/"+shortName)
.then(response => {
if (response.data.success) {
dispatch(deleteSystemSuccess({shortName: shortName}));
dispatch(push("/list-systems"));
} else {
dispatch(deleteSystemError());
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 createSystem(data) {
return dispatch => {
dispatch(beginCreateSystem());
return makeUserRequest("post", data, "/systems")
.then(response => {
if (response.data.success) {
dispatch(createSystemSuccess(response.data.system));
} else {
dispatch(createSystemError());
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

@@ -0,0 +1,16 @@
export const CREATE_SYSTEM = "CREATE_SYSTEM"
export const CREATE_SUCCESS_SYSTEM = "CREATE_SUCCESS_SYSTEM"
export const CREATE_ERROR_SYSTEM = "CREATE_ERROR_SYSTEM"
export const UPDATE_SYSTEM = "UPDATE_SYSTEM"
export const UPDATE_SUCCESS_SYSTEM = "UPDATE_SUCCESS_SYSTEM"
export const UPDATE_ERROR_SYSTEM = "UPDATE_ERROR_SYSTEM"
export const DELETE_SYSTEM = "DELETE_SYSTEM"
export const DELETE_SUCCESS_SYSTEM = "DELETE_SUCCESS_SYSTEM"
export const DELETE_ERROR_SYSTEM = "DELETE_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 FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
export const FETCH_ERROR = "FETCH_ERROR"
export const FETCH_SUCCESS_ERROR = "FETCH_SUCCESS_ERROR"
export const FETCH_ERROR_ERROR = "FETCH_ERROR_ERROR"

View File

@@ -0,0 +1,95 @@
import {
CREATE_SYSTEM,
CREATE_SUCCESS_SYSTEM,
CREATE_ERROR_SYSTEM,
UPDATE_SYSTEM,
UPDATE_SUCCESS_SYSTEM,
UPDATE_ERROR_SYSTEM,
DELETE_SYSTEM,
DELETE_SUCCESS_SYSTEM,
DELETE_ERROR_SYSTEM,
FETCH_SYSTEM,
FETCH_SUCCESS_SYSTEM,
FETCH_ERROR_SYSTEM
} 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 CREATE_SYSTEM:
return Object.assign({}, state, { isWaiting: true});
case DELETE_SUCCESS_SYSTEM:
return Object.assign({}, state, {
items: state.items.filter(item => item.shortName !== action.data.shortName)
});
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 CREATE_SUCCESS_SYSTEM:
const newItems = state.items.concat({
name: action.data.name,
shortName: action.data.shortName,
description: action.data.description,
systemType: action.data.systemType,
city: action.data.city,
state: action.data.state,
county: action.data.county,
country: action.data.country,
userId: action.data.userId,
id: action.data.id,
showScreenName: action.data.showScreenName
});
return Object.assign({}, state, { isWaiting: false,
items: newItems});
case UPDATE_ERROR_SYSTEM:
case FETCH_ERROR_SYSTEM:
case CREATE_ERROR_SYSTEM:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default system;

View File

@@ -0,0 +1,66 @@
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 ListTalkgroups 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 talkgroups = this.props.talkgroups;
if (talkgroups) {
return (
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Mode</Table.HeaderCell>
<Table.HeaderCell>Number</Table.HeaderCell>
<Table.HeaderCell>Alpha</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{talkgroups.map((talkgroup, i) => (
<Table.Row key={talkgroup.num + "-" + i}>
<Table.Cell>{talkgroup.mode}</Table.Cell>
<Table.Cell>{talkgroup.num}</Table.Cell>
<Table.Cell>{talkgroup.alpha}</Table.Cell>
<Table.Cell>{talkgroup.description}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
);
} else {
return (
<div/>
);
}
}
}
export default ListTalkgroups;

View File

@@ -0,0 +1,181 @@
import axios, { post } from 'axios';
//import { browserHistory } from "react-router"
import * as types from "./talkgroup-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginImportTalkgroup() {
return { type: types.IMPORT_TALKGROUP };
}
function importTalkgroupSuccess(data) {
return {
type: types.IMPORT_SUCCESS_TALKGROUP,
data
};
}
function importTalkgroupError(data) {
return {
type: types.IMPORT_ERROR_TALKGROUP,
data
};
}
function beginExportTalkgroup() {
return { type: types.EXPORT_TALKGROUP };
}
function exportTalkgroupSuccess() {
return { type: types.EXPORT_SUCCESS_TALKGROUP };
}
function exportTalkgroupError() {
return { type: types.EXPORT_ERROR_TALKGROUP };
}
function beginFetchTalkgroup() {
return { type: types.FETCH_TALKGROUP };
}
function fetchTalkgroupSuccess(data) {
return { type: types.FETCH_SUCCESS_TALKGROUP,
data };
}
function fetchTalkgroupError(data) {
return { type: types.FETCH_ERROR_TALKGROUP,
data };
}
function makeUserRequest(method, data, api) {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// https://stackoverflow.com/questions/41878838/how-do-i-set-multipart-in-axios-with-react
function fileUpload(shortName, file){
const url = "/talkgroups/" + shortName + "/import";
const formData = new FormData();
formData.append('file',file)
const config = {
headers: {
'content-type': 'multipart/form-data'
}
}
return post(url, formData,config)
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
export function importTalkgroups(
shortName, file,
successPath // path to redirect to upon successful log in
) {
return dispatch => {
dispatch(beginImportTalkgroup());
return fileUpload(shortName, file)
.then(response => {
if (response.data.success) {
var data = {};
data.shortName = shortName;
data.talkgroups = response.data.talkgroups
dispatch(importTalkgroupSuccess(data));
// 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)
} else {
dispatch(importTalkgroupError(response.data.message));
return response.data.message;
}
})
.catch(function(response) {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
return response.message;
}
});
};
}
export function exportTalkgroups(
shortName
) {
return dispatch => {
dispatch(beginExportTalkgroup());
return axios
.get("/talkgroups/" + shortName + "/export")
.then(response => {
if (response.data.success) {
var data = {shortName: shortName, talkgroups: response.data.talkgroups}
dispatch(fetchTalkgroupSuccess( data));
} else {
dispatch(fetchTalkgroupError());
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 shouldFetchTalkgroups(state, shortName) {
if (!state.talkgroup.items.hasOwnProperty(shortName)) {
return true
} else if (state.talkgroup.isWaiting) {
return false
} else {
return false
}
}
export function fetchTalkgroups(shortName) {
return (dispatch, getState) => {
if (shouldFetchTalkgroups(getState(), shortName)) {
dispatch(beginFetchTalkgroup());
return axios
.get("/talkgroups/"+shortName)
.then(response => {
if (response.data.success) {
var data = {shortName: shortName, talkgroups: response.data.talkgroups}
dispatch(fetchTalkgroupSuccess( data));
} else {
dispatch(fetchTalkgroupError());
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

@@ -0,0 +1,9 @@
export const IMPORT_TALKGROUP = "IMPORT_TALKGROUP"
export const IMPORT_SUCCESS_TALKGROUP = "IMPORT_SUCCESS_TALKGROUP"
export const IMPORT_ERROR_TALKGROUP = "IMPORT_ERROR_TALKGROUP"
export const EXPORT_TALKGROUP = "EXPORT_TALKGROUP"
export const EXPORT_SUCCESS_TALKGROUP = "EXPORT_SUCCESS_TALKGROUP"
export const EXPORT_ERROR_TALKGROUP = "EXPORT_ERROR_TALKGROUP"
export const FETCH_TALKGROUP = "FETCH_TALKGROUP"
export const FETCH_SUCCESS_TALKGROUP = "FETCH_SUCCESS_TALKGROUP"
export const FETCH_ERROR_TALKGROUP = "FETCH_ERROR_TALKGROUP"

View File

@@ -0,0 +1,72 @@
import {
IMPORT_TALKGROUP,
IMPORT_SUCCESS_TALKGROUP,
IMPORT_ERROR_TALKGROUP,
EXPORT_TALKGROUP,
EXPORT_SUCCESS_TALKGROUP,
EXPORT_ERROR_TALKGROUP,
FETCH_TALKGROUP,
FETCH_SUCCESS_TALKGROUP,
FETCH_ERROR_TALKGROUP
} from "./talkgroup-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, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
const talkgroup = (
state = {
isWaiting: false,
items: {}},
action
) => {
switch (action.type) {
case FETCH_TALKGROUP:
case IMPORT_TALKGROUP:
case EXPORT_TALKGROUP:
return Object.assign({}, state, { isWaiting: true});
case IMPORT_SUCCESS_TALKGROUP:
case FETCH_SUCCESS_TALKGROUP:
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: action.data.talkgroups
}
});
case FETCH_ERROR_TALKGROUP:
case IMPORT_ERROR_TALKGROUP:
case EXPORT_ERROR_TALKGROUP:
case EXPORT_SUCCESS_TALKGROUP:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default talkgroup;

View File

@@ -0,0 +1,46 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./user-constants";
import { push } from 'connected-react-router';
// "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 };
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
export function manualLogout() {
return dispatch => {
dispatch(beginLogout());
return axios
.get(account_server + "/logout", {withCredentials: true})
.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);
}
});
};
}

View File

@@ -0,0 +1,6 @@
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 LOGOUT_USER = "LOGOUT_USER"
export const LOGOUT_SUCCESS_USER = "LOGOUT_SUCCESS_USER"
export const LOGOUT_ERROR_USER = "LOGOUT_ERROR_USER"

View File

@@ -0,0 +1,55 @@
import {
LOGOUT_USER,
LOGOUT_SUCCESS_USER,
LOGOUT_ERROR_USER,
LOGIN_SUCCESS_USER,
LOGIN_ERROR_USER,
LOGIN_EMAIL_ERROR_USER
} from "./user-constants";
const user = (
state = {
isWaiting: false,
authenticated: false,
userId: ""
},
action
) => {
switch (action.type) {
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 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
});
default:
return state;
}
};
export default user;

View File

@@ -0,0 +1,80 @@
import React from "react"
import { render } from "react-dom"
import { Provider } from "react-redux"
import { Switch, Route} from 'react-router-dom'
import { applyMiddleware, createStore } from 'redux'
import { routerMiddleware } from 'connected-react-router'
import { ConnectedRouter } from 'connected-react-router'
import thunk from "redux-thunk"
import {createLogger} from "redux-logger"
import isNode from "detect-node"
import createRootReducer from './reducers'
import { createBrowserHistory } from 'history'
import About from "./components/pure/About"
import App from "./components/container/App"
import NavigationContainer from "./components/container/NavigationContainer"
import Restricted from "./components/container/Restricted"
// System
import SystemContainer from "./System/SystemContainer"
import EditSystemContainer from "./System/EditSystemContainer"
import CreateSystemContainer from "./System/CreateSystemContainer"
import ListSystemsContainer from "./System/ListSystemsContainer"
// Create a history of your choosing (we're using a browser history in this case)
export const history = createBrowserHistory()
// 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 navigatin
const store = createStore(
createRootReducer(history), // root reducer with router state
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 path="/about" component={About} />
<Route exact path="/" component={Restricted(ListSystemsContainer, store)} />
{/* -- System -- */}
<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)} />
</Switch>
</App>
</div>
</ConnectedRouter>
</Provider>
, document.getElementById("app"))

View File

@@ -0,0 +1,19 @@
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

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,56 @@
// 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(account_server + "/authenticated", {withCredentials: true})
.then(response => {
if (response.data.success) {
const user = response.data.user
store.dispatch({
type: types.LOGIN_SUCCESS_USER,
data: user
});
} else {
window.location = account_server + "/login?nextLocation=admin";
/*history.replace({ pathname: '/login',
state: { nextPathname: params.location.pathname } });*/
}
})
.catch(response => {
if (response instanceof Error) {
window.location = account_server + "/login?nextLocation=admin";
// Something happened during logout that triggered an Error
console.log("Error", response.message);
}
});
}
}
render() {
return <BaseComponent {...this.props} />;
}
}
return withRouter(Restricted);
}

View File

@@ -0,0 +1,42 @@
import React, { Component } from "react";
import { Container, Header } from "semantic-ui-react";
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
var admin_email = process.env['ADMIN_EMAIL'] != null ? process.env['ADMIN_EMAIL'] : "luke@openmhz.com";
class About extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Container text>
<Header size='large'>Share the Air</Header>
<p>{site_name} makes it easy to share and archive recordings from <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a>.
Sharing access to these radio systems allows for other members of your community to follow local events.
Raising awareness of what our local fire, police and EMS have to go through everyday should
lead to greater appreciation for all the work they do that goes largely unseen.</p>
<p>Each of the radio systems comes from different contributers around the country. The different
systems may come and go as new ones are added and taken down. {site_name} only maintains the archive that
is available online.</p>
<p> - Luke <a href="mailto:{admin_email}?Subject={site_name}" target="_top">{admin_email}</a> </p>
</Container>
);
}
}
export default About;

View File

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,62 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Menu, Dropdown, Icon, Header } from "semantic-ui-react";
const navStyle = {
marginBottom: "30px"
};
const blankStyle = {
marginBottom: "50px"
};
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : 'https://account.openmhz.com';
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
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>
{site_name}
</Menu.Item>
<Menu.Menu position="right">
<Link to="/list-systems">
<Menu.Item name="systems"> Systems</Menu.Item>
</Link>
<Link to="/about">
<Menu.Item name="about">About</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>
) : (
<div style={blankStyle}/>
)}
</div>
);
}
}
export default Navigation;

View File

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

View File

@@ -0,0 +1,21 @@
import { combineReducers } from "redux"
import { connectRouter } from 'connected-react-router'
import system from "../System/system-reducer"
import statistic from "../System/statistic-reducer"
import talkgroup from "../Talkgroups/talkgroup-reducer"
import group from "../Group/group-reducer"
import error from "../System/error-reducer"
import permission from "../Permission/permission-reducer"
import user from "../User/user-reducer"
export default (history) => combineReducers({
router: connectRouter(history),
system,
talkgroup,
statistic,
error,
group,
permission,
user
})

View File

@@ -0,0 +1,86 @@
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';
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://socket.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';
export default function(app, passport) {
app.set("port", 3008)
// 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.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(express.static(path.join(process.cwd(), 'public')));
const sess = {
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
}*/
app.use(session(sess))
app.use(passport.initialize())
app.use(passport.session())
app.use('/*', function(req, res, next) {
var allowedOrigins = [ account_server];
allowedOrigins.push(frontend_server);
allowedOrigins.push(backend_server);
allowedOrigins.push(socket_server);
var origin = req.headers.origin;
if (allowedOrigins.indexOf(origin) > -1) {
res.setHeader('Access-Control-Allow-Origin', origin);
} else if (req.headers["user-agent"] == 'TrunkRecorder1.0') {
res.setHeader('Access-Control-Allow-Origin', "*");
} else {
res.setHeader('Access-Control-Allow-Origin', "*");
if (origin) {
console.warn("forcing CORS for: " + origin + " referer: " + req.headers.referer);
}
}
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,WEBSOCKET');
res.header('Access-Control-Allow-Credentials', 'true');
res.header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Max-Age");
res.header('Access-Control-Max-Age', '600');
next();
});
}

View File

@@ -0,0 +1,36 @@
/*
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

@@ -0,0 +1,26 @@
/* 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

@@ -0,0 +1,15 @@
/** 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

@@ -0,0 +1,297 @@
import Group from "../models/group";
import System from "../models/system";
import mongoose from "mongoose";
import passport from "passport";
//router.get('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) {
exports.getGroups = function(req, res, next) {
process.nextTick(function() {
System.findOne(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, system) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!system) {
console.error(err);
res.json({
success: false,
message: "That Short Name does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
res.json({
success: false,
message: "You are not the user associated with this system."
});
return;
}
if (req.params.groupId) {
var groupId = req.params.groupId.toLowerCase();
Group.findOne(
{
_id: new mongoose.Types.ObjectId(groupId),
shortName: req.params.shortName.toLowerCase()
},
{ sort: { position: -1 } },
function(err, group) {
if (err) {
res.json({
success: false,
message: err
});
return;
} else {
res.json({
success: true,
group: group
});
return;
}
}
);
} else {
Group.find(
{
shortName: req.params.shortName.toLowerCase()
}).sort("position").exec(
function(err, groups) {
if (err) {
res.json({
success: false,
message: err
});
return;
} else {
res.json({
success: true,
groups: groups
});
return;
}
}
);
}
}
);
});
};
exports.deleteGroup = function(req, res, next) {
//router.get('/system/:shortName/remove_group/:groupId', isLoggedIn, function(req, res) {
System.findOne({
'shortName': req.params.shortName.toLowerCase()
}, function(err, system) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!system) {
console.error(err);
res.json({
success: false,
message: "That Short Name does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
res.json({
success: false,
message: "You are not the user associated with this system."
});
return;
}
Group.findOneAndRemove({
shortName: req.params.shortName.toLowerCase(),
'_id': new mongoose.Types.ObjectId(req.params.groupId)
}, function(err) {
if (err) {
res.json({
success: false,
message: err
});
return;
} else {
res.json({
success: true
});
return;
}
// removed!
});
});
};
exports.reorderGroups = function(req, res, next) {
//router.post('/system/:shortName/group_order', isLoggedIn, function(req, res) {
process.nextTick(function() {
System.findOne(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, system) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!system) {
console.error(err);
res.json({
success: false,
message: "That Short Name does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
res.json({
success: false,
message: "You are not the user associated with this system."
});
return;
}
var groupOrder = JSON.parse(req.body.groupOrder);
for (var i = 0; i < groupOrder.length; i++) {
console.log("in group: " + groupOrder[i] + " position: " + i);
Group.findOneAndUpdate(
{
_id: new mongoose.Types.ObjectId(groupOrder[i]),
shortName: req.params.shortName.toLowerCase()
},
{
position: i
},
{
new: true,
upsert: false
},
function(err, group) {
console.log("Save group: " + group._id + " position: " + group.position);
if (err) {
res.json({
success: false,
message: err
});
console.log(
"Error: Group Order - " +
err +
" Shortname: " +
req.params.shortName.toLowerCase()
);
return;
}
}
);
}
res.json({
success: true
});
}
);
});
};
exports.upsertGroup = function(req, res, next) {
//router.post('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) {
process.nextTick(function() {
System.findOne(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, system) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!system) {
console.error(err);
res.json({
success: false,
message: "That Short Name does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
res.json({
success: false,
message: "You are not the user associated with this system."
});
return;
}
var message = "";
var groupName = req.body.groupName.replace(/[^\w\s]/g, "");
var talkgroups = JSON.parse(req.body.talkgroups);
console.log(
"Talkgroups: " +
talkgroups +
" for group: " +
groupName +
" array: " +
Array.isArray(talkgroups)
);
if (!talkgroups.length || !Array.isArray(talkgroups)) {
message = "Talkgroups need to be selected, group can not be empty ";
}
if (!groupName.length) {
message = message + "Group Name is required";
}
if (message.length) {
res.json({
success: false,
message: message
});
return;
}
// if we got here, that means that everything is good for updating/inserting
var groupId = req.params.groupId;
Group.findOneAndUpdate(
{
_id: new mongoose.Types.ObjectId(groupId),
shortName: req.params.shortName.toLowerCase()
},
{
groupName: groupName,
talkgroups: talkgroups
},
{
new: true,
upsert: true
},
function(err, group) {
if (err) {
res.json({
success: false,
message: err
});
return;
} else {
res.json({
success: true,
group: group
});
return;
}
}
);
}
);
});
};

View File

@@ -0,0 +1,243 @@
import mongoose from "mongoose";
import passport from "passport";
import Permission from "../models/permission";
import System from "../models/system";
import User from "../models/user";
exports.fetchPermissions = function(req, res, next) {
Permission.find(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, permissions) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
var userIds = permissions.map(permission => {return permission.userId});
User.find(
{
_id: {
$in: userIds
}
},
function(err, users) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
var returnUsers = users.map(user =>{
var permission = permissions.find(obj => {
if (obj.userId.toString() == user._id.toString()) {
return true;
}
return false;
});
if (!permission) {
console.error("Error - Permission not found for user: " + user._id);
res.json({ success: false, message: "Permission not found" });
return;
}
return {_id: permission._id, userId: user._id, firstName: user.firstName, lastName: user.lastName, email: user.email, role: permission.role };
});
res.json({ success: true, permissions: returnUsers });
});
return;
}
);
};
exports.addPermission = async (req, res, next) => {
//router.post('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) {
let role = req.body.role;
var system = false;
var user = false;
var existing = false;
var saved = false;
if (!role || (role >= 20) || (role <1)) {
res.json({
success: false,
message: "Invalid Role"
});
return;
}
console.log("role looks good");
try {
system = await System.findOne(
{
shortName: req.params.shortName.toLowerCase()
});
} catch(err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!system) {
res.json({
success: false,
message: "That System does not exist."
});
return;
}
console.log("system looks good")
try {
user = await User.findOne(
{
email: req.body.email
});
} catch (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!user) {
res.json({ success: false, message: "User not found" });
return;
}
console.log("user looks good")
try {
existing = await Permission.findOne({userId: user._id, systemId: system._id});
} catch (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (existing) {
res.json({ success: false, message: "User already has acccess" });
return;
}
console.log("no existing")
var permission = new Permission({
userId: user._id,
systemId: system._id,
shortName: system.shortName,
role: role
});
try {
saved = await permission.save()
} catch(err) {
res.json({
success: false,
message: err
});
return;
}
console.log("saved it")
var newPermission = saved.toObject();
newPermission.firstName = user.firstName;
newPermission.lastName = user.lastName;
newPermission.email = user.email;
console.log("Add Permission to: " + system.shortName + " for: " + newPermission.email + " by: " + req.user.email);
res.json({
success: true,
permission: newPermission
});
};
exports.updatePermission = function(req, res, next) {
process.nextTick(function() {
Permission.findOne(
{
_id: new mongoose.Types.ObjectId(req.params.permissionId),
shortName: req.params.shortName.toLowerCase(req.params.shortName)
},
function(err, permission) {
if (err) {
res.json({
success: false,
message: err
});
return;
}
if (permission.role >= 20) {
res.json({
success: false,
message: "Cannot change permissions of system owner"
});
return;
}
var newRole = req.body.role;
if (!newRole || (newRole >= 20) || (newRole <1)) {
res.json({
success: false,
message: "Invalid Role"
});
return;
}
permission.role = newRole;
permission.save(function(err, saved) {
if (err) {
res.json({
success: false,
message: err
});
return;
}
res.json({
success: true,
permission: saved.toJSON()
});
return;
});
}
);
});
};
exports.deletePermission = function(req, res, next) {
//router.get('/system/:shortName/remove_group/:groupId', isLoggedIn, function(req, res) {
Permission.findOneAndRemove({
shortName: req.params.shortName.toLowerCase(),
'_id': new mongoose.Types.ObjectId(req.params.permissionId),
role: { $lt : 20}
}, function(err, permission) {
if (err) {
res.json({
success: false,
message: err
});
return;
}
if (!permission) {
res.json({
success: false,
message: "Permission not found"
});
return;
} else {
res.json({
success: true
});
return;
}
// removed!
});
};

View File

@@ -0,0 +1,509 @@
import mongoose from "mongoose";
import passport from "passport";
import System from "../models/system";
import SystemStat from "../models/system_stat";
import Talkgroup from "../models/talkgroup";
import Group from "../models/group";
import secrets from "../config/secrets";
import uuid from 'node-uuid';
const crypto = require("crypto");
exports.isLoggedIn = function (req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect(account_server + "/login");
};
// -------------------------------------------
exports.listSystems = function (req, res, next) {
console.log("Listing Systems for: " + req.user._id);
System.find({
userId: mongoose.Types.ObjectId(req.user._id)
},
function (err, systems) {
if (err) {
console.error("Error - ListSystems: " + err);
res.json({
success: false,
message: err
});
return;
}
var shortNames = new Array();
for (var i = 0; i < systems.length; i++) {
shortNames.push(systems[i].shortName);
}
SystemStat.find({
shortName: {
$in: shortNames
}
},
function (err, stats) {
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);
if (obj.showScreenName) {
rObj.screenName = req.user.screenName;
} else {
rObj.screenName = null;
}
rObj.id = obj._id;
return rObj;
});
var sys_stats = {}
var MS_PER_MINUTE = 60000;
var now = new Date();
for (var i = 0; i < stats.length; i++) {
sys_stats[stats[i].shortName] = {};
sys_stats[stats[i].shortName].callTotals = stats[i].callTotals;
sys_stats[stats[i].shortName].errorTotals = stats[i].errorTotals;
sys_stats[stats[i].shortName].errorStats = stats[i].errorStats;
sys_stats[stats[i].shortName].usageBytes = stats[i].usageBytes;
sys_stats[stats[i].shortName].usageMb = stats[i].usageMb;
}
res.json({
success: true,
systems: returnSys,
stats: sys_stats
});
return;
}
);
}
);
};
function remove_system(shortName) {
/*
var sysDir = config.mediaDirectory + "/" + shortName + "/"
console.log("Removing DIR: " + sysDir);
fs.remove(sysDir, function(err) {
if (err) return console.error(err)
});
Transmission.remove({
shortName: shortName
}, function(err) {
if (err) return console.error(err);
// removed!
});
*/
System.remove({
shortName: shortName
},
function (err) {
if (err) return console.error(err);
// removed!
}
);
Talkgroup.remove({
shortName: shortName
},
function (err) {
if (err) return console.error(err);
// removed!
}
);
Group.remove({
shortName: shortName
},
function (err) {
if (err) return console.error(err);
// removed!
}
);
}
exports.deleteSystem = function (req, res, next) {
//router.get('/delete_system/:shortName', isLoggedIn, function(req, res) {
System.findOne({
shortName: req.params.shortName.toLowerCase()
},
function (err, system) {
if (err) {
res.json({
success: false,
message: err
});
return;
}
if (!system) {
res.json({
success: false,
message: "That Short Name does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
console.log("Error deleting syste - Wrong User - sys: " + system.userId + " profile: " + mongoose.Types.ObjectId(req.user._id));
res.json({
success: false,
message: "You are not the user associated with this system."
});
return;
}
console.log("Removing: " + system.name);
remove_system(system.shortName);
res.json({
success: true
});
return;
}
);
};
// -------------------------------------------
exports.ownSystem = async function (req, res, next) {
try {
var system = await System.findOne({
shortName: req.body.shortName.toLowerCase()
});
if (!system) {
console.error("ERROR: OwnSystem - system does not exist: " + req.body.shortName.toLowerCase());
res.json({
success: false,
message: "That System does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
console.error("ERROR: OwnSystem - not users system " + req.body.shortName.toLowerCase());
res.json({
success: false,
message: "You are not the user associated with this system."
});
console.error("System: " + system.userId + " User: " + req.user._id);
return;
}
res.locals.test = "Yoho!"
res.locals.system = system;
console.log(system);
console.log(res.locals);
next();
} catch (err) {
console.error("ERROR: Unique ShortName - caught: " + err);
res.json({
success: false,
message: err
});
return;
}
}
// -------------------------------------------
exports.uniqueShortName = async function (req, res, next) {
try {
var system = await System.findOne({
shortName: res.locals.shortName
});
if (system) {
console.error("ERROR: Unique Shortname already in use: " + req.body.shortName.toLowerCase());
res.json({
success: false,
message: "Short Name already in use"
});
return;
} else {
next();
}
} catch (err) {
console.error("ERROR: Unique ShortName - caught: " + err);
res.json({
success: false,
message: err
});
return;
}
}
// -------------------------------------------
exports.updateSystem = async function (req, res, next) {
console.log(res.locals)
res.locals.system.name = res.locals.name;
res.locals.system.city = res.locals.city;
res.locals.system.state = res.locals.state;
res.locals.system.county = res.locals.county;
res.locals.system.country = res.locals.country;
res.locals.system.description = res.locals.description;
res.locals.system.systemType = res.locals.systemType;
res.locals.system.showScreenName = res.locals.showScreenName;
res.locals.system.save(function (err) {
if (err) {
res.json({
success: false,
message: err
});
return;
} else {
var returnSys = (({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
showScreenName
}) => ({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
key,
showScreenName
}))(res.locals.system);
returnSys.id = res.locals.system._id;
res.json({
success: true,
system: returnSys
});
return;
}
});
}
// -------------------------------------------
exports.validateSystem = async function (req, res, next) {
try {
res.locals.showScreenName = req.body.showScreenName;
if (!req.body.name || (req.body.name.length < 2)) {
console.error("ERROR: Validate System - req.body.name");
res.json({
success: false,
message: "System Name is Required"
});
return;
}
res.locals.name = req.body.name.replace(/[^\w\s\.\,\-\_]/gi, '');
if (!req.body.description || (req.body.description.length < 2)) {
console.error("ERROR: Validate System - req.body.description");
res.json({
success: false,
message: "System Description is Required"
});
return;
}
res.locals.description = req.body.description.replace(/[^\w\s\.\,\-\_]/gi, '');
if (!req.body.shortName || (req.body.shortName.length < 2)) {
console.error("ERROR: Validate System - req.body.shortName");
res.json({
success: false,
message: "System ShortName is Required"
});
return;
}
var tempShortName = req.body.shortName.replace(/[^\w]/gi, '');
res.locals.shortName = tempShortName.toLowerCase();
// -------------------- Handle all the different SystemTypes of Locations
// make sure it is one of the expected types
if ((req.body.systemType != "state") && (req.body.systemType != "city") && (req.body.systemType != "county") && (req.body.systemType != "international")) {
console.error("ERROR: Validate System - req.body.systemType: " + req.body.systemType);
res.json({
success: false,
message: "Select system location"
});
return;
}
// copy it over
res.locals.systemType = req.body.systemType;
// now check to make sure the correct things for location is filled out
if (req.body.systemType === "state" ||
req.body.systemType === "city" ||
req.body.systemType === "county") {
if (!req.body.state || (req.body.state === "") || (req.body.state.length < 2)) {
console.error("ERROR: Validate System - req.body.state: " + req.body.state);
res.json({
success: false,
message: "Select the State for the System"
});
return;
}
res.locals.state = req.body.state;
}
if (req.body.systemType === "city") {
if (!req.body.city || (req.body.city === "") || (req.body.city.length < 2)) {
console.error("ERROR: Validate System - req.body.systemType: " + req.body.systemType);
res.json({
success: false,
message: "Enter the city for the System"
});
return;
}
res.locals.city = req.body.city.replace(/[^\w\s\.\,\-\_]/gi, '');
}
if (req.body.systemType === "county") {
if (!req.body.county || (req.body.county === "") || (req.body.county.length < 2)) {
console.error("ERROR: Validate System - req.body.county: " + req.body.county);
res.json({
success: false,
message: "Enter the county for the System"
});
return;
}
res.locals.county = req.body.county.replace(/[^\w\s\.\,\-\_]/gi, '');
}
if (req.body.systemType === "international") {
if (!req.body.country || (req.body.country === "") || (req.body.country.length < 2)) {
console.error("ERROR: Validate System - req.body.country: " + req.body.country);
res.json({
success: false,
message: "Enter the country for the System"
});
return;
}
res.locals.country = req.body.country.replace(/[^\w\s\.\,\-\_]/gi, '');
}
} catch (err) {
console.error("ERROR: Validate System - caught: " + err);
res.json({
success: false,
message: err
});
return;
}
next();
}
// -------------------------------------------
exports.createSystem = async function (req, res, next) {
var key = uuid();
key = crypto
.createHash("sha256")
.update(key)
.update("salt")
.digest("hex");
const system = (({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
showScreenName
}) => ({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
showScreenName
}))(res.locals);
system.key = key;
system.userId = mongoose.Types.ObjectId(req.user._id);
System.create(system, function (err, newSys) {
if (err) {
console.error(err);
res.json({
success: false,
message: err
});
return;
}
var returnSys = (({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
showScreenName,
key
}) => ({
name,
shortName,
description,
systemType,
city,
state,
county,
country,
userId,
showScreenName,
key
}))(newSys);
returnSys.id = newSys._id;
res.json({
success: true,
system: returnSys
});
return;
});
}

View File

@@ -0,0 +1,253 @@
import mongoose from "mongoose";
import passport from "passport";
import Talkgroup from "../models/talkgroup";
import System from "../models/system";
import secrets from "../config/secrets";
import fs from 'fs-extra';
const crypto = require("crypto");
var parse = require('csv-parse');
exports.isLoggedIn = function(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect(account_server + "/login");
};
//router.get("/tg_export/:shortName", isLoggedIn, function(req, res) {
exports.exportTalkgroups = function(req, res, next) {
process.nextTick(function() {
System.findOne(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, system) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!system) {
console.error(err);
res.json({
success: false,
message: "That Short Name does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
res.json({
success: false,
message: "You are not the user associated with this system."
});
return;
}
Talkgroup.find(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, talkgroups) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
if (!talkgroups || !Array.isArray(talkgroups)) {
console.error(err);
res.json({
success: false,
message: "Talkgroups were not found."
});
return;
}
res.statusCode = 200;
res.setHeader("Content-Type", "text/csv");
talkgroups.forEach(function(item) {
/* res.write('"' + item.num + '", "'
+ item.mode + '", "'
+ item.alpha + '", "'
+ item.description + '", "'
+ item.tag + '", "'
+ item.group + '", "'
+ item.priority+ '"\r\n');*/
if (item) {
var sending = "";
sending = item.num + ", ";
if (item.mode) {
sending = sending + item.mode.toString().replace(/\,/g, "") + ", ";
} else {
sending = sending + " ,";
}
if (item.alpa) {
sending = sending + item.alpha.toString().replace(/\,/g, "") + ", ";
} else {
sending = sending + " ,";
}
if (item.description) {
sending = sending + item.description.toString().replace(/\,/g, "") + ", ";
} else {
sending = sending + " ,";
}
if (item.tag) {
sending = sending + item.tag.toString().replace(/\,/g, "") + ", ";
} else {
sending = sending + " ,";
}
if (item.group) {
sending = sending + item.group.toString().replace(/\,/g, "") + ", ";
} else {
sending = sending + " ,";
}
if (item.priority) {
sending = sending + item.priority.toString().replace(/\,/g, "") + "\r\n";
} else {
sending = sending + "\r\n";
}
res.write(sending);
}
});
res.end();
}
);
}
);
});
};
function csv_import(shortName, filename, callback) {
var talkgroups = [];
Talkgroup.remove(
{
shortName: shortName
},
function(err) {
if (err) {
console.error("Error: csv import: " + err);
callback(err, null);
} else {
var parser = parse(
{
columns: [
"num",
"mode",
"alpha",
"description",
"tag",
"group",
"priority"
]
},
async function(err, data) {
if (err) {
console.error("Error: csv import 2: " + err);
callback(err, null);
} else {
for (var i = 0; i < data.length; i++) {
var row = data[i];
var newTg = new Talkgroup();
newTg.num = parseInt(row.num);
newTg.alpha = row.alpha;
newTg.mode = row.mode;
newTg.description = row.description;
newTg.tag = row.tag.toLowerCase();
newTg.group = row.group.toLowerCase();
newTg.priority = row.priority;
newTg.shortName = shortName;
await newTg.save(function(err) {
if (err) {
console.log("Error: CSV import: " + err);
callback(err, null);
}
});
}
callback(null, data);
}
}
);
}
fs.createReadStream(filename).pipe(parser);
// removed!
}
);
}
//router.post("/tg_import/:shortName", isLoggedIn, upload.single('file'), function(req, res, next) {
exports.importTalkgroups = function(req, res, next) {
process.nextTick(function() {
System.findOne(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, system) {
if (err) {
console.error("Error - importTalkgroups: " + err);
res.json({ success: false, message: err });
return;
}
if (!system) {
console.error("Error - importTalkgroups: System not found - " + req.params.shortName.toLowerCase());
res.json({
success: false,
message: "That Short Name does not exist."
});
return;
}
if (!system.userId.equals(req.user._id)) {
console.error("Error - importTalkgroups: you are not the user for hte system: " + req.user._id);
res.json({
success: false,
message: "You are not the user associated with this system."
});
return;
}
if (!req.file || req.file.size === 0) {
res.json({ success: false, message: "Please select a file." });
return;
}
fs.exists(req.file.path, function(exists) {
if (exists) {
csv_import(system.shortName, req.file.path, function(err, data) {
if (err) {
console.error("Erorr - CSV import, callback: " + err);
res.json({ success: false, message: err });
res.end();
return;
} else {
res.json({ success: true, talkgroups: data });
return;
}
});
} else {
console.error("Error - importTalkgroups: no file");
res.end(
"Well, there is no magic for those who don’t believe in it!"
);
}
});
}
);
});
};
// -------------------------------------------
exports.fetchTalkgroups = function(req, res, next) {
Talkgroup.find(
{
shortName: req.params.shortName.toLowerCase()
},
function(err, talkgroups) {
if (err) {
console.error(err);
res.json({ success: false, message: err });
return;
}
res.json({ success: true, talkgroups: talkgroups });
return;
}
);
};

218
admin/src/server/index.js Normal file
View File

@@ -0,0 +1,218 @@
import fs from "fs"
import mongoose from "mongoose"
import express from "express"
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 systems from "./controllers/systems"
import groups from "./controllers/groups"
import permissions from "./controllers/permissions"
import talkgroups from "./controllers/talkgroups"
import Permission from "./models/permission";
import "./models/user"
import multer from 'multer'
var upload = multer({
dest: 'uploads/'
});
// -------------------------------------------
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))
}
/*
async function getRole(req) {
let promise = new Promise((resolve, reject) => {
if (req.params.shortName && req.user) {
var short_name = req.params.shortName.toLowerCase();
var userId = req.user._id;
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
if (err) {
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
reject(-1);
}
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
resolve(permission.role);
});
} else {
reject(-1);
}});
return promise;
}*/
function isOwner(req, res, next) {
if (req.params.shortName && req.user) {
var short_name = req.params.shortName.toLowerCase();
var userId = req.user._id;
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
if (err) {
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
}
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
if ( permission.role >= 15) {
return next();
} else {
res.status(401).send({
success: false,
message: "Insufficent Permission, role: " + permission.role
});
}
});
}
}
function isAdmin(req, res, next) {
if (req.params.shortName && req.user) {
var short_name = req.params.shortName.toLowerCase();
var userId = req.user._id;
Permission.findOne({'userId': userId, 'shortName': short_name}, function(err, permission) {
if (err) {
console.error('Error - getRole userId: ' + userId + ' shortName: ' + short_name + ' error: ' + err);
}
console.log('Found - getRole userId: ' + userId + ' shortName: ' + short_name + ' role: ' + permission.role);
if (permission.role >= 10) {
return next();
} else {
res.status(401).send({
success: false,
message: "Insufficent Permission, role: " + permission.role
});
}
});
}
}
function isLoggedIn(req, res, next) {
if (req.isAuthenticated()) return next();
res.status(401).send({
success: false,
message: "Not Authenticated."
});
};
var admin_server = process.env['ADMIN_SERVER'] != null ? process.env['ADMIN_SERVER'] : 'https://admin.openmhz.com';
var beta_frontend_server = process.env['BETA_FRONTEND_SERVER'] != null ? process.env['BETA_FRONTEND_SERVER'] : 'https://beta.openmhz.com';
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 media_server = process.env['MEDIA_SERVER'] != null ? process.env['MEDIA_SERVER'] : 'https://media.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
var socket_server = process.env['SOCKET_SERVER'] != null ? process.env['SOCKET_SERVER'] : 'wss://api.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;
// -------------------------------------------
configurePassport(app, passport)
configureExpress(app, passport)
// -------------------------------------------
app.use(express.static(path.join(__dirname, "public")));
app.get("")
//app.get("/permissions/:shortname", isOwner, permissions.fetchPermissions)
/*app.get("/permissions/:shortName", permissions.fetchPermissions);
app.delete("/permissions/:shortName/:permissionId", isAdmin, permissions.deletePermission);
app.post("/permissions/:shortName/:permissionId", isAdmin, permissions.updatePermission);
app.post("/permissions/:shortName", isAdmin, permissions.addPermission);*/
app.get("/talkgroups/:shortName", isLoggedIn, talkgroups.fetchTalkgroups)
app.post("/talkgroups/:shortName/import", isLoggedIn, upload.single('file'), talkgroups.importTalkgroups)
app.get("/talkgroups/:shortName/export", isLoggedIn, talkgroups.exportTalkgroups)
app.post("/groups/:shortName/reorder", isLoggedIn, groups.reorderGroups);
app.post("/groups/:shortName/:groupId?", isLoggedIn, groups.upsertGroup);
app.get("/groups/:shortName/:groupId?", isLoggedIn, groups.getGroups);
app.delete("/groups/:shortName/:groupId", isLoggedIn, groups.deleteGroup);
app.get("/systems", isLoggedIn, systems.listSystems)
app.delete("/systems/:shortName", isLoggedIn, systems.deleteSystem)
app.post("/systems/:shortName", [isLoggedIn, systems.ownSystem, systems.validateSystem, systems.updateSystem])
app.post("/systems", [isLoggedIn, systems.uniqueShortName, systems.validateSystem, systems.createSystem])
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>${site_name} Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<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 admin_server = "${admin_server}";
const backend_server = "${backend_server}";
const media_server = "${media_server}";
const socket_server = "${socket_server}";
const account_server = "${account_server}";
const proPlanValue = ${proPlanValue};
const freePlanValue = ${freePlanValue};
</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

@@ -0,0 +1,14 @@
var mongoose = require('mongoose');
var groupSchema = mongoose.Schema({
shortName: String,
groupName: String,
groupId: String,
position: Number,
talkgroups: [Number]
});
module.exports = mongoose.model('Group', groupSchema);

View File

@@ -0,0 +1,24 @@
// Defining a User Model in mongoose
// Code modified from https://github.com/sahat/hackathon-starter
var mongoose = require("mongoose");
const PermissionSchema = new mongoose.Schema({
userId: mongoose.Schema.Types.ObjectId,
systemId: mongoose.Schema.Types.ObjectId,
shortName: String,
role: {
type: Number,
default: 0
},
ver: {
type: Number,
default: 1.2
}
})
module.exports=mongoose.model("Permission", PermissionSchema)

View File

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,12 @@
var mongoose = require('mongoose');
var systemStatSchema = mongoose.Schema({
callTotals: [Number],
errorTotals: [Number],
usageBytes: Number,
usageMb: Number,
shortName: String
}, { collection: 'system_stats' });
module.exports = mongoose.model('SystemStat', systemStatSchema);

View File

@@ -0,0 +1,17 @@
var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');
var talkgroupSchema = mongoose.Schema({
shortName: String,
userId: String,
num: Number,
mode: String,
alpha: String,
description: String,
tag: String,
group: String,
priority: Number
});
module.exports = mongoose.model('Talkgroup', talkgroupSchema);

View File

@@ -0,0 +1,92 @@
// 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

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