Admin is mostly working

This commit is contained in:
Luke Berndt
2021-01-04 22:22:39 -05:00
parent 0deadc385f
commit c8b4f02d06
158 changed files with 17683 additions and 3661 deletions

39
admin/src/App.css Normal file
View File

@@ -0,0 +1,39 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-float infinite 3s ease-in-out;
}
}
.App-header {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.App-link {
color: rgb(112, 76, 182);
}
@keyframes App-logo-float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(10px)
}
100% {
transform: translateY(0px)
}
}

58
admin/src/App.js Normal file
View File

@@ -0,0 +1,58 @@
import React from 'react';
import logo from './logo.svg';
import { Counter } from './features/counter/Counter';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Counter />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<span>
<span>Learn </span>
<a
className="App-link"
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
>
React
</a>
<span>, </span>
<a
className="App-link"
href="https://redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux
</a>
<span>, </span>
<a
className="App-link"
href="https://redux-toolkit.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux Toolkit
</a>
,<span> and </span>
<a
className="App-link"
href="https://react-redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
React Redux
</a>
</span>
</header>
</div>
);
}
export default App;

15
admin/src/App.test.js Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import store from './app/store';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(
<Provider store={store}>
<App />
</Provider>
);
expect(getByText(/learn/i)).toBeInTheDocument();
});

View File

@@ -2,10 +2,6 @@ import React, { Component } from "react"
import NavigationContainer from "./NavigationContainer"
class App extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div>

View File

@@ -1,6 +1,6 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Menu, Dropdown, Icon, Header } from "semantic-ui-react";
import { Menu, Dropdown } from "semantic-ui-react";
const navStyle = {
marginBottom: "30px"
@@ -21,37 +21,32 @@ class Navigation extends Component {
};
render() {
var profileLink = account_server + "/profile";
var billingLink = account_server + "/billing";
var profileLink = process.env.REACT_APP_ACCOUNT_SERVER + "/profile";
return (
<div>
{this.props.user.authenticated ? (
<Menu style={navStyle}>
<Menu.Item name="systems" header>
{site_name}
</Menu.Item>
<Menu.Item name="systems" header>
{ process.env.REACT_APP_SITE_NAME}
</Menu.Item>
<Menu.Menu position="right">
<Link to="/list-systems">
<Menu.Item name="systems"> Systems</Menu.Item>
</Link>
<Link to="/about">
<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 href={profileLink}>Profile</Dropdown.Item>
<Dropdown.Item onClick={this._logout}>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Menu.Menu>
</Menu>
) : (
<div style={blankStyle}/>
)}
<div style={blankStyle} />
)}
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { connect } from "react-redux"
import * as userActions from "../../User/user-actions"
import Navigation from "../pure/Navigation"
import * as userActions from "../User/user-actions"
import Navigation from "./Navigation"
// Function passed in to `connect` to subscribe to Redux store updates.

View File

@@ -1,13 +1,12 @@
// 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 * as types from "../User/user-constants";
import axios from "axios";
/**
* Higher-order component (HOC) to wrap restricted pages
*/
export default (BaseComponent, store) => {
const restricted = (BaseComponent, store) => {
class Restricted extends Component {
componentWillMount() {
this.checkAuthentication(this.props);
@@ -24,7 +23,7 @@ export default (BaseComponent, store) => {
const authenticated = store.getState().user.authenticated;
if (!authenticated) {
axios
.get(account_server + "/authenticated", {withCredentials: true})
.get(process.env.REACT_APP_ACCOUNT_SERVER + "/authenticated", {withCredentials: true})
.then(response => {
if (response.data.success) {
const user = response.data.user
@@ -33,14 +32,14 @@ export default (BaseComponent, store) => {
data: user
});
} else {
window.location = account_server + "/login?nextLocation=admin";
window.location = process.env.REACT_APP_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";
window.location = process.env.REACT_APP_ACCOUNT_SERVER + "/login?nextLocation=admin";
// Something happened during logout that triggered an Error
console.log("Error", response.message);
}
@@ -54,3 +53,4 @@ export default (BaseComponent, store) => {
}
return withRouter(Restricted);
}
export default restricted

View File

@@ -4,33 +4,18 @@ import { Container, Header } from "semantic-ui-react";
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>.
<Header size='large'>Share the Air</Header>
<p>{process.env.REACT_APP_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
<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. {process.env.REACT_APP_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>
<p> - Luke <a href="mailto:{process.env.REACT_APP_ADMIN_EMAIL}?Subject={process.env.REACT_APP_SITE_NAME}" target="_top">{process.env.REACT_APP_ADMIN_EMAIL}</a> </p>
</Container>
);
}

View File

@@ -17,11 +17,6 @@ import {
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 => {
@@ -73,7 +68,7 @@ const group = (
});
case DELETE_SUCCESS_GROUP:
const delGroups = state.items[action.data.shortName].filter(x => x._id != action.data.groupId);
const delGroups = state.items[action.data.shortName].filter(x => x._id !== action.data.groupId);
return Object.assign({}, state, {
isWaiting: false,
items: {

View File

@@ -1,45 +1,19 @@
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() {

View File

@@ -260,7 +260,7 @@ class System extends Component {
}
handleUpgradePlan() {
window.location = account_server + "/update-plans";
window.location = process.env.REACT_APP_ACCOUNT_SERVER + "/update-plans";
}
handleUpload(file) {
@@ -374,7 +374,7 @@ class System extends Component {
'",\n\t\t\t"apiKey": "' +
system.key +
'"\n\t\t}\n\t],\n\t"uploadServer": "' +
backend_server +
process.env.REACT_APP_BACKEND_SERVER +
'"\n}';
switch (system.systemType) {
@@ -457,7 +457,7 @@ class System extends Component {
</Segment>
<Segment>
<Header size="small">Upload Server</Header>
{backend_server}
{process.env.REACT_APP_BACKEND_SERVER}
</Segment>
<Segment>
<Header size="small">Example</Header>

View File

@@ -3,36 +3,22 @@ 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;
@@ -46,6 +32,8 @@ render() {
case "international":
location = system.country;
break;
default:
location = "unknown";
}
}
return (
@@ -57,7 +45,7 @@ render() {
{system.name}
<Header.Subheader>{system.screenName}</Header.Subheader>
</Header.Content>
</Header>{plan&&<Label color='orange' ribbon='right'>{plan}</Label>}
</Header>
</Card.Content><Card.Content>
<Card.Description>{system.description}</Card.Description>

View File

@@ -5,7 +5,7 @@ import {
Header
} from "semantic-ui-react";
class EditSystem extends Component {
class UpdateSystem extends Component {
constructor(props) {
super(props);
this.handleSystemSubmit = this.handleSystemSubmit.bind(this);
@@ -45,4 +45,4 @@ class EditSystem extends Component {
}
}
export default EditSystem
export default UpdateSystem

View File

@@ -1,5 +1,5 @@
import { connect } from "react-redux"
import EditSystem from "./EditSystem"
import UpdateSystem from "./UpdateSystem"
import * as systemActions from "./system-actions"
@@ -15,4 +15,4 @@ const mapStateToProps = (state, props) => {
export default connect(
mapStateToProps,
systemActions
)(EditSystem)
)(UpdateSystem)

View File

@@ -39,7 +39,7 @@ export function fetch(shortName) {
dispatch(beginFetch());
return axios
.get(backend_server + "/" + shortName + "/errors")
.get(process.env.REACT_APP_BACKEND_SERVER + "/" + shortName + "/errors")
.then(response => {
if (response.data) {
var data ={

View File

@@ -67,7 +67,7 @@ function fetchSystemError(data) {
data };
}
function beginFetchErrors() {
function beginFetchError() {
return { type: types.FETCH_ERROR };
}
@@ -103,11 +103,9 @@ export function updateSystem(data) {
return dispatch => {
dispatch(beginUpdateSystem());
return makeUserRequest("post", data, "/systems/" + data.shortName)
return makeUserRequest("post", data, process.env.REACT_APP_BACKEND_SERVER + "/systems/" + data.shortName)
.then(response => {
if (response.data.success) {
const user = response.data.user
dispatch(updateSystemSuccess(response.data.system));
} else {
dispatch(updateSystemError());
@@ -139,7 +137,7 @@ export function fetchErrors(shortName) {
dispatch(beginFetchError());
return axios
.get(backend_server + "/" + shortName + "/systems")
.get(process.env.REACT_APP_BACKEND_SERVER + "/" + shortName + "/systems")
.then(response => {
if (response.data) {
dispatch(fetchErrorsSuccess(shortName, response.data));
@@ -164,7 +162,7 @@ export function fetchSystems() {
dispatch(beginFetchSystem());
return axios
.get("/systems")
.get(process.env.REACT_APP_ADMIN_SERVER + "/systems")
.then(response => {
if (response.data.success) {
dispatch(fetchSystemSuccess(response.data.systems));
@@ -189,7 +187,7 @@ export function deleteSystem(shortName) {
return dispatch => {
dispatch(beginDeleteSystem());
return makeUserRequest("delete", null, "/systems/"+shortName)
return makeUserRequest("delete", null, process.env.REACT_APP_BACKEND_SERVER + "/systems/"+shortName)
.then(response => {
if (response.data.success) {
dispatch(deleteSystemSuccess({shortName: shortName}));
@@ -213,7 +211,7 @@ export function createSystem(data) {
return dispatch => {
dispatch(beginCreateSystem());
return makeUserRequest("post", data, "/systems")
return makeUserRequest("post", data, process.env.REACT_APP_BACKEND_SERVER + "/systems")
.then(response => {
if (response.data.success) {
dispatch(createSystemSuccess(response.data.system));

View File

@@ -13,12 +13,6 @@ import {
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) {
@@ -43,6 +37,7 @@ const system = (
) => {
switch (action.type) {
case DELETE_SYSTEM:
case UPDATE_SYSTEM:
case FETCH_SYSTEM:
case CREATE_SYSTEM:
@@ -80,6 +75,7 @@ const system = (
return Object.assign({}, state, { isWaiting: false,
items: newItems});
case DELETE_ERROR_SYSTEM:
case UPDATE_ERROR_SYSTEM:
case FETCH_ERROR_SYSTEM:
case CREATE_ERROR_SYSTEM:

View File

@@ -10,27 +10,6 @@ import {
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,

View File

@@ -24,7 +24,7 @@ export function manualLogout() {
dispatch(beginLogout());
return axios
.get(account_server + "/logout", {withCredentials: true})
.get(process.env.REACT_APP_ACCOUNT_SERVER + "/logout", {withCredentials: true})
.then(response => {
if (response.data.success) {
dispatch(logoutSuccess());

View File

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

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

View File

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

View File

@@ -1,21 +0,0 @@
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,60 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount,
} from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState('2');
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() =>
dispatch(incrementByAmount(Number(incrementAmount) || 0))
}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
>
Add Async
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,74 @@
.row {
display: flex;
align-items: center;
justify-content: center;
}
.row:not(:last-child) {
margin-bottom: 16px;
}
.value {
font-size: 78px;
padding-left: 16px;
padding-right: 16px;
margin-top: 2px;
font-family: 'Courier New', Courier, monospace;
}
.button {
appearance: none;
background: none;
font-size: 32px;
padding-left: 12px;
padding-right: 12px;
outline: none;
border: 2px solid transparent;
color: rgb(112, 76, 182);
padding-bottom: 4px;
cursor: pointer;
background-color: rgba(112, 76, 182, 0.1);
border-radius: 2px;
transition: all 0.15s;
}
.textbox {
font-size: 32px;
padding: 2px;
width: 64px;
text-align: center;
margin-right: 8px;
}
.button:hover, .button:focus {
border: 2px solid rgba(112, 76, 182, 0.4);
}
.button:active {
background-color: rgba(112, 76, 182, 0.2);
}
.asyncButton {
composes: button;
position: relative;
margin-left: 8px;
}
.asyncButton:after {
content: "";
background-color: rgba(112, 76, 182, 0.15);
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
opacity: 0;
transition: width 1s linear, opacity 0.5s ease 1s;
}
.asyncButton:active:after {
width: 0%;
opacity: 1;
transition: 0s
}

View File

@@ -0,0 +1,42 @@
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount));
}, 1000);
};
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state => state.counter.value;
export default counterSlice.reducer;

13
admin/src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

37
admin/src/index.js Normal file
View File

@@ -0,0 +1,37 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { Route, Switch } from 'react-router' // react-router v4/v5
import { ConnectedRouter } from 'connected-react-router'
import configureStore, { history } from './redux-router/configureStore'
import 'semantic-ui-css/semantic.min.css'
import App from "./App/App"
import Restricted from "./App/Restricted"
import About from "./Components/About"
import ListSystemsContainer from "./System/ListSystemsContainer"
import UpdateSystemContainer from "./System/UpdateSystemContainer"
import CreateSystemContainer from "./System/CreateSystemContainer"
const store = configureStore(/* provide initial state if any */)
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
<>
<App>
<Switch>
<Route path="/about" component={About} />
<Route path="/update-system/:shortName" component={UpdateSystemContainer} />
<Route path="/create-system" component={CreateSystemContainer} />
<Route path="/list-systems" component={ListSystemsContainer} />
<Route render={() => (<div>Miss</div>)} />
</Switch>
</App>
</>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
)

1
admin/src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g fill="#764ABC"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,26 @@
// https://github.com/supasate/connected-react-router
// configureStore.js
import { createBrowserHistory } from 'history'
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'connected-react-router'
import createRootReducer from './reducers'
import thunk from 'redux-thunk';
export const history = createBrowserHistory()
export default function configureStore(preloadedState) {
const store = createStore(
createRootReducer(history), // root reducer with router state
preloadedState,
compose(
applyMiddleware(
routerMiddleware(history), // for dispatching history actions
thunk,
// ... other middlewares ...
),
),
)
return store
}

View File

@@ -0,0 +1,23 @@
// from https://github.com/supasate/connected-react-router
import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
import systemReducer from "../System/system-reducer"
import talkgroupReducer from "../Talkgroups/talkgroup-reducer"
import groupReducer from "../Group/group-reducer"
import errorReducer from "../System/error-reducer"
import permissionReducer from "../Permission/permission-reducer"
import userReducer from "../User/user-reducer"
const createRootReducer = (history) => combineReducers({
router: connectRouter(history),
system: systemReducer,
talkgroup: talkgroupReducer,
group: groupReducer,
user: userReducer,
error: errorReducer,
permission: permissionReducer
})
export default createRootReducer

View File

@@ -1,86 +0,0 @@
import path from "path"
import express from "express"
import session from "express-session"
import bodyParser from "body-parser"
import connectMongo from "connect-mongo"
import secrets from "./secrets"
const MongoStore = connectMongo(session)
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
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

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

View File

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

View File

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

View File

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

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

@@ -1,510 +0,0 @@
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) {
console.error("locals: " + res.locals.shortName + " body: " + req.body.shortName )
try {
var system = await System.findOne({
shortName: res.locals.shortName.toLowerCase()
});
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

@@ -1,253 +0,0 @@
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;
}
);
};

View File

@@ -1,221 +0,0 @@
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 admin_email = process.env['ADMIN_EMAIL'] != null ? process.env['ADMIN_EMAIL'] : "luke@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 site_name = "${site_name}";
const admin_email = "${admin_email}";
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

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

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

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

View File

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

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

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

View File

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

137
admin/src/serviceWorker.js Normal file
View File

@@ -0,0 +1,137 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

5
admin/src/setupTests.js Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';