Compare commits
1 Commits
master
...
update-dep
Author | SHA1 | Date | |
---|---|---|---|
c12f242cfa |
@ -0,0 +1,3 @@
|
|||||||
|
cmdline: /usr/lib/wireshark/extcap/sshdump --capture --extcap-interface sshdump --fifo /tmp/wireshark_extcap_sshdump_20180903010220_5gyEKt --remote-host RouterMain --remote-port 22 --remote-username root --sshkey /home/alex/.ssh/id_rsa --remote-interface eth0 --remote-capture-command --remote-sudo false --remote-filter not ((host fe80::9880:27ff:fec4:20a8 or host fe80::42:cff:fecd:a672 or host fe80::42:ff:fecd:47fc or host fe80::42:f7ff:fee1:ae70 or host fe80::ea43:5b74:219d:c5b7 or host 2001:470:b:a14:5751:93a2:2f5f:b9a0 or host fd7d:e461:6dfd:0:d0a6:7939:471:f8ff or host fd7d:e461:6dfd::c22 or host 2001:470:b:a14::c22 or host 172.19.0.1 or host 172.18.0.1 or host 172.17.0.1 or host 192.168.8.10) and port 22) --remote-count 0 --debug false --debug-file
|
||||||
|
Remote capture command has disabled other options
|
||||||
|
Running:
|
@ -5,8 +5,8 @@ WORKDIR /app/
|
|||||||
COPY package.json yarn.lock /app/
|
COPY package.json yarn.lock /app/
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
COPY tsconfig.json tslint.json paths.js /app/
|
COPY tslint.json /app
|
||||||
COPY bin/ /app/bin
|
COPY paths.js /app
|
||||||
COPY client/ /app/client
|
COPY client/ /app/client
|
||||||
COPY common/ /app/common
|
COPY common/ /app/common
|
||||||
COPY server/ /app/server
|
COPY server/ /app/server
|
||||||
@ -19,11 +19,10 @@ FROM node:10
|
|||||||
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
COPY --from=builder /app/package.json /app/yarn.lock /app/paths.js ./
|
COPY --from=builder /app/package.json /app/yarn.lock ./
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/bin ./bin
|
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT [ "node", ".", "serve"]
|
ENTRYPOINT [ "node", "." ]
|
||||||
|
@ -5,7 +5,8 @@ WORKDIR /app/
|
|||||||
COPY package.json yarn.lock /app/
|
COPY package.json yarn.lock /app/
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
COPY tsconfig.json tslint.json paths.js /app/
|
COPY paths.js /app
|
||||||
|
COPY tslint.json /app
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT [ "npm", "run", "start:dev" ]
|
ENTRYPOINT [ "npm", "run", "start:dev" ]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// import DevTools from "mobx-react-devtools";
|
// import DevTools from "mobx-react-devtools";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Redirect, Route, Switch, withRouter } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { Container } from "semantic-ui-react";
|
import { Container } from "semantic-ui-react";
|
||||||
|
|
||||||
import { MessagesView, NavBar } from "@client/components";
|
import { MessagesView, NavBar } from "@client/components";
|
||||||
@ -30,7 +30,7 @@ function NavContainer() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={route.login} component={p.LoginPage} />
|
<Route path={route.login} component={p.LoginPage} />
|
||||||
@ -39,5 +39,3 @@ function App() {
|
|||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(App);
|
|
||||||
|
@ -2,7 +2,7 @@ import * as classNames from "classnames";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Dimmer, Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react";
|
import { Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react";
|
||||||
|
|
||||||
import { DeviceImage } from "@client/components";
|
import { DeviceImage } from "@client/components";
|
||||||
import * as p from "@client/pages";
|
import * as p from "@client/pages";
|
||||||
@ -62,7 +62,7 @@ const ConnectionState = observer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
interface DeviceViewProps extends RouteComponentProps {
|
interface DeviceViewProps {
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
inList?: boolean;
|
inList?: boolean;
|
||||||
@ -87,13 +87,11 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { connectionState, sectionRunner, sections } = this.device;
|
const { connectionState, sectionRunner, sections } = this.device;
|
||||||
const dimmed = !connectionState.isDeviceConnected;
|
|
||||||
if (!connectionState.isAvailable || inList) {
|
if (!connectionState.isAvailable || inList) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Dimmer.Dimmable blurring dimmed={dimmed} className="device-body">
|
<React.Fragment>
|
||||||
<Dimmer active={dimmed} />
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Column mobile="16" tablet="16" computer="16" largeScreen="6">
|
<Grid.Column mobile="16" tablet="16" computer="16" largeScreen="6">
|
||||||
<SectionRunnerView
|
<SectionRunnerView
|
||||||
@ -117,7 +115,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
|||||||
path={route.program(":deviceId", ":programId")}
|
path={route.program(":deviceId", ":programId")}
|
||||||
component={p.ProgramPage}
|
component={p.ProgramPage}
|
||||||
/>
|
/>
|
||||||
</Dimmer.Dimmable>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,4 +187,4 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(injectState(observer(DeviceView)));
|
export default injectState(observer(DeviceView));
|
||||||
|
@ -6,90 +6,13 @@ import { Duration } from "@common/Duration";
|
|||||||
|
|
||||||
import "@client/styles/DurationView";
|
import "@client/styles/DurationView";
|
||||||
|
|
||||||
export interface DurationViewProps {
|
export default class DurationView extends React.Component<{
|
||||||
label?: string;
|
label?: string;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
duration: Duration;
|
duration: Duration;
|
||||||
onDurationChange?: (newDuration: Duration) => void;
|
onDurationChange?: (newDuration: Duration) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}> {
|
||||||
|
|
||||||
function roundOrString(val: number | string): number | string {
|
|
||||||
if (typeof val === "number") {
|
|
||||||
return Math.round(val);
|
|
||||||
} else {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NumberInputProps {
|
|
||||||
className?: string;
|
|
||||||
label?: string;
|
|
||||||
value: number;
|
|
||||||
max?: number;
|
|
||||||
onChange: (value: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NumberInput(props: NumberInputProps): React.ReactElement {
|
|
||||||
const [valueState, setValueState] = React.useState<number | string>(props.value);
|
|
||||||
const [elementId, setElementId] = React.useState(() => `NumberInput-${Math.round(Math.random() * 100000000)}`);
|
|
||||||
const [isWheelChange, setIsWheelChange] = React.useState(false);
|
|
||||||
|
|
||||||
const onChange: InputProps["onChange"] = (_e, data) => {
|
|
||||||
setValueState(data.value);
|
|
||||||
const newValue = parseFloat(data.value);
|
|
||||||
if (!isNaN(newValue) && data.value.length > 0 && isWheelChange) {
|
|
||||||
props.onChange(Math.round(newValue));
|
|
||||||
setIsWheelChange(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBlur: React.FocusEventHandler = () => {
|
|
||||||
const newValue = (typeof valueState === "number") ? valueState : parseFloat(valueState);
|
|
||||||
if (!props.onChange || isNaN(newValue)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (props.value !== newValue) {
|
|
||||||
props.onChange(Math.round(newValue));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onWheel = (e: Event) => {
|
|
||||||
// do nothing
|
|
||||||
setIsWheelChange(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const el = document.getElementById(elementId);
|
|
||||||
if (el) {
|
|
||||||
// Not passive events
|
|
||||||
el.addEventListener("wheel", onWheel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (props.value !== valueState) {
|
|
||||||
setValueState(props.value);
|
|
||||||
}
|
|
||||||
}, [props.value]);
|
|
||||||
|
|
||||||
return <Input
|
|
||||||
id={elementId}
|
|
||||||
type="number"
|
|
||||||
pattern="[0-9\.]*" // for safari
|
|
||||||
inputMode="numeric"
|
|
||||||
value={roundOrString(valueState)}
|
|
||||||
onChange={onChange}
|
|
||||||
// onMouseOut={onBlur}
|
|
||||||
onBlur={onBlur}
|
|
||||||
className={props.className}
|
|
||||||
label={props.label}
|
|
||||||
max={props.max}
|
|
||||||
labelPosition="right"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DurationView extends React.Component<DurationViewProps> {
|
|
||||||
render() {
|
render() {
|
||||||
const { duration, label, inline, onDurationChange, className } = this.props;
|
const { duration, label, inline, onDurationChange, className } = this.props;
|
||||||
const inputsClassName = classNames("durationInputs", { inline });
|
const inputsClassName = classNames("durationInputs", { inline });
|
||||||
@ -99,18 +22,24 @@ export default class DurationView extends React.Component<DurationViewProps> {
|
|||||||
<Form.Field inline={inline} className={className}>
|
<Form.Field inline={inline} className={className}>
|
||||||
{label && <label>{label}</label>}
|
{label && <label>{label}</label>}
|
||||||
<div className={inputsClassName}>
|
<div className={inputsClassName}>
|
||||||
<NumberInput
|
<Input
|
||||||
|
type="number"
|
||||||
className="durationInput minutes"
|
className="durationInput minutes"
|
||||||
value={this.props.duration.minutes}
|
value={duration.minutes}
|
||||||
onChange={this.onMinutesChange}
|
onChange={this.onMinutesChange}
|
||||||
label="M"
|
label="M"
|
||||||
|
labelPosition="right"
|
||||||
|
onWheel={this.onWheel}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<Input
|
||||||
|
type="number"
|
||||||
className="durationInput seconds"
|
className="durationInput seconds"
|
||||||
value={this.props.duration.seconds}
|
value={duration.seconds}
|
||||||
onChange={this.onSecondsChange}
|
onChange={this.onSecondsChange}
|
||||||
max={60}
|
max="60"
|
||||||
label="S"
|
label="S"
|
||||||
|
labelPosition="right"
|
||||||
|
onWheel={this.onWheel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
@ -126,25 +55,23 @@ export default class DurationView extends React.Component<DurationViewProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: Readonly<DurationViewProps>) {
|
private onMinutesChange: InputProps["onChange"] = (e, { value }) => {
|
||||||
if (nextProps.duration.minutes !== this.props.duration.minutes ||
|
if (!this.props.onDurationChange || isNaN(Number(value))) {
|
||||||
nextProps.duration.seconds !== this.props.duration.seconds) {
|
return;
|
||||||
this.setState({
|
|
||||||
minutes: nextProps.duration.minutes,
|
|
||||||
seconds: nextProps.duration.seconds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMinutesChange = (newMinutes: number) => {
|
|
||||||
if (this.props.onDurationChange) {
|
|
||||||
this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
|
|
||||||
}
|
}
|
||||||
|
const newMinutes = Number(value);
|
||||||
|
this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSecondsChange = (newSeconds: number) => {
|
private onSecondsChange: InputProps["onChange"] = (e, { value }) => {
|
||||||
if (this.props.onDurationChange) {
|
if (!this.props.onDurationChange || isNaN(Number(value))) {
|
||||||
this.props.onDurationChange(this.props.duration.withSeconds(newSeconds));
|
return;
|
||||||
}
|
}
|
||||||
|
const newSeconds = Number(value);
|
||||||
|
this.props.onDurationChange(this.props.duration.withSeconds(newSeconds));
|
||||||
|
};
|
||||||
|
|
||||||
|
private onWheel = () => {
|
||||||
|
// do nothing
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ class MessageView extends React.Component<{
|
|||||||
if (message.onDismiss) {
|
if (message.onDismiss) {
|
||||||
message.onDismiss(event, data);
|
message.onDismiss(event, data);
|
||||||
}
|
}
|
||||||
uiStore.removeMessage(message);
|
uiStore.messages.remove(message);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,17 +105,16 @@ class ProgramSequenceItem extends React.Component<{
|
|||||||
|
|
||||||
const ProgramSequenceItemD = SortableElement(ProgramSequenceItem);
|
const ProgramSequenceItemD = SortableElement(ProgramSequenceItem);
|
||||||
|
|
||||||
// tslint:disable: no-shadowed-variable
|
|
||||||
const ProgramSequenceList = SortableContainer(
|
const ProgramSequenceList = SortableContainer(
|
||||||
observer(
|
observer(
|
||||||
function ProgramSequenceList(props: {
|
(props: {
|
||||||
className: string;
|
className: string;
|
||||||
list: ProgramItem[];
|
list: ProgramItem[];
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
onChange: ItemChangeHandler;
|
onChange: ItemChangeHandler;
|
||||||
onRemove: ItemRemoveHandler;
|
onRemove: ItemRemoveHandler;
|
||||||
}) {
|
}) => {
|
||||||
const { className, list, sections, ...rest } = props;
|
const { className, list, sections, ...rest } = props;
|
||||||
const listItems = list.map((item, index) => {
|
const listItems = list.map((item, index) => {
|
||||||
const key = `item-${index}`;
|
const key = `item-${index}`;
|
||||||
@ -133,6 +132,7 @@ const ProgramSequenceList = SortableContainer(
|
|||||||
return <ul className={className}>{listItems}</ul>;
|
return <ul className={className}>{listItems}</ul>;
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
{ withRef: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
@ -8,7 +8,6 @@ import { ProgramSequenceView, ScheduleView } from "@client/components";
|
|||||||
import * as route from "@client/routePaths";
|
import * as route from "@client/routePaths";
|
||||||
import { ISprinklersDevice } from "@common/httpApi";
|
import { ISprinklersDevice } from "@common/httpApi";
|
||||||
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
||||||
import moment = require("moment");
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class ProgramRows extends React.Component<{
|
class ProgramRows extends React.Component<{
|
||||||
@ -70,12 +69,6 @@ class ProgramRows extends React.Component<{
|
|||||||
<h4>Sequence: </h4>{" "}
|
<h4>Sequence: </h4>{" "}
|
||||||
<ProgramSequenceView sequence={sequence} sections={sections} />
|
<ProgramSequenceView sequence={sequence} sections={sections} />
|
||||||
<ScheduleView schedule={schedule} label={<h4>Schedule: </h4>} />
|
<ScheduleView schedule={schedule} label={<h4>Schedule: </h4>} />
|
||||||
<h4 className="program--nextRun">Next run: </h4>
|
|
||||||
{
|
|
||||||
program.nextRun
|
|
||||||
? <time title={moment(program.nextRun).toString()}>{moment(program.nextRun).fromNow()}</time>
|
|
||||||
: <time title="never">never</time>
|
|
||||||
}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Form, Header, Icon, Popup, Segment } from "semantic-ui-react";
|
import { Form, Header, Icon, Segment } from "semantic-ui-react";
|
||||||
|
|
||||||
import { DurationView, SectionChooser } from "@client/components";
|
import { DurationView, SectionChooser } from "@client/components";
|
||||||
import { UiStore } from "@client/state";
|
import { UiStore } from "@client/state";
|
||||||
@ -30,14 +30,8 @@ export default class RunSectionForm extends React.Component<
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { sectionId, duration } = this.state;
|
const { sectionId, duration } = this.state;
|
||||||
const runButton = (
|
|
||||||
<Form.Button primary onClick={this.run} disabled={!this.isValid} className="runSectionForm-runButton">
|
|
||||||
<Icon name="play" />
|
|
||||||
Run
|
|
||||||
</Form.Button>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Segment className="runSectionForm">
|
<Segment>
|
||||||
<Header>Run Section</Header>
|
<Header>Run Section</Header>
|
||||||
<Form>
|
<Form>
|
||||||
<SectionChooser
|
<SectionChooser
|
||||||
@ -51,14 +45,10 @@ export default class RunSectionForm extends React.Component<
|
|||||||
duration={duration}
|
duration={duration}
|
||||||
onDurationChange={this.onDurationChange}
|
onDurationChange={this.onDurationChange}
|
||||||
/>
|
/>
|
||||||
{
|
<Form.Button primary onClick={this.run} disabled={!this.isValid}>
|
||||||
this.isValid ? runButton :
|
<Icon name="play" />
|
||||||
<Popup trigger={runButton} on={["click", "hover"]} position="right center">
|
Run
|
||||||
<Popup.Content>
|
</Form.Button>
|
||||||
Select a section to run and a duration
|
|
||||||
</Popup.Content>
|
|
||||||
</Popup>
|
|
||||||
}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Button, Item } from "semantic-ui-react";
|
import { Item } from "semantic-ui-react";
|
||||||
|
|
||||||
import { DeviceView } from "@client/components";
|
import { DeviceView } from "@client/components";
|
||||||
import { AppState, injectState } from "@client/state";
|
import { AppState, injectState } from "@client/state";
|
||||||
|
|
||||||
class DevicesPage extends React.Component<{ appState: AppState }> {
|
class DevicesPage extends React.Component<{ appState: AppState }> {
|
||||||
refreshDevices = () => {
|
|
||||||
this.props.appState.sprinklersRpc.doAuthenticate();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { appState } = this.props;
|
const { appState } = this.props;
|
||||||
const userData = appState.userStore.getUserData();
|
const { userData } = appState.userStore;
|
||||||
let deviceNodes: React.ReactNode;
|
let deviceNodes: React.ReactNode;
|
||||||
if (!userData) {
|
if (!userData) {
|
||||||
deviceNodes = <span>Not logged in</span>;
|
deviceNodes = <span>Not logged in</span>;
|
||||||
@ -25,7 +21,7 @@ class DevicesPage extends React.Component<{ appState: AppState }> {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<h1 className="devices-header">Devices <Button icon="refresh" onClick={this.refreshDevices} /></h1>
|
<h1>Devices</h1>
|
||||||
<Item.Group>{deviceNodes}</Item.Group>
|
<Item.Group>{deviceNodes}</Item.Group>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { assign } from "lodash";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as qs from "query-string";
|
import * as qs from "query-string";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { RouteComponentProps, withRouter } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
CheckboxProps,
|
CheckboxProps,
|
||||||
@ -20,9 +20,7 @@ import { AppState, injectState } from "@client/state";
|
|||||||
import { ISprinklersDevice } from "@common/httpApi";
|
import { ISprinklersDevice } from "@common/httpApi";
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
||||||
import classNames = require("classnames");
|
|
||||||
import { action } from "mobx";
|
import { action } from "mobx";
|
||||||
import * as moment from "moment";
|
|
||||||
|
|
||||||
interface ProgramPageProps
|
interface ProgramPageProps
|
||||||
extends RouteComponentProps<{ deviceId: string; programId: string }> {
|
extends RouteComponentProps<{ deviceId: string; programId: string }> {
|
||||||
@ -175,10 +173,8 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
|
|
||||||
const { running, enabled, schedule, sequence } = program;
|
const { running, enabled, schedule, sequence } = program;
|
||||||
|
|
||||||
const className = classNames("programEditor", editing && "editing");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open onClose={this.close} className={className}>
|
<Modal open onClose={this.close} className="programEditor">
|
||||||
<Modal.Header>{this.renderName(program)}</Modal.Header>
|
<Modal.Header>{this.renderName(program)}</Modal.Header>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<Form>
|
<Form>
|
||||||
@ -187,6 +183,7 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
toggle
|
toggle
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
|
readOnly={!editing}
|
||||||
onChange={this.onEnabledChange}
|
onChange={this.onEnabledChange}
|
||||||
/>
|
/>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
@ -211,16 +208,6 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
editing={editing}
|
editing={editing}
|
||||||
label={<h4>Schedule</h4>}
|
label={<h4>Schedule</h4>}
|
||||||
/>
|
/>
|
||||||
{ !editing && (
|
|
||||||
<h4 className="program--nextRun">Next run: </h4>)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!editing && (
|
|
||||||
program.nextRun
|
|
||||||
? <time title={moment(program.nextRun).toString()}>{moment(program.nextRun).fromNow()}</time>
|
|
||||||
: <time title="never">never</time>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
{this.renderActions(program)}
|
{this.renderActions(program)}
|
||||||
@ -253,10 +240,6 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
log.error({ err }, "error updating Program");
|
log.error({ err }, "error updating Program");
|
||||||
this.props.appState.uiStore.addMessage({
|
|
||||||
error: true,
|
|
||||||
content: `Error updating program: ${err}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.stopEditing();
|
this.stopEditing();
|
||||||
@ -282,28 +265,11 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
|
|
||||||
@action.bound
|
@action.bound
|
||||||
private onEnabledChange(e: any, p: CheckboxProps) {
|
private onEnabledChange(e: any, p: CheckboxProps) {
|
||||||
if (p.checked !== undefined && this.program) {
|
if (this.programView) {
|
||||||
this.program.enabled = p.checked;
|
this.programView.enabled = p.checked!;
|
||||||
this.program.update().then(
|
|
||||||
data => {
|
|
||||||
log.info({ data }, "Program updated");
|
|
||||||
this.props.appState.uiStore.addMessage({
|
|
||||||
success: true,
|
|
||||||
content: `Program ${this.program!.name} ${this.program!.enabled ? "enabled" : "disabled"}`,
|
|
||||||
timeout: 2000,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
log.error({ err }, "error updating Program");
|
|
||||||
this.props.appState.uiStore.addMessage({
|
|
||||||
error: true,
|
|
||||||
content: `Error updating program: ${err}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DecoratedProgramPage = injectState(withRouter(ProgramPage));
|
const DecoratedProgramPage = injectState(observer(ProgramPage));
|
||||||
export default DecoratedProgramPage;
|
export default DecoratedProgramPage;
|
||||||
|
@ -92,10 +92,6 @@ export class WebSocketRpcClient extends s.SprinklersRPC {
|
|||||||
this._connect();
|
this._connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect() {
|
|
||||||
this._connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.reconnectTimer != null) {
|
if (this.reconnectTimer != null) {
|
||||||
clearTimeout(this.reconnectTimer);
|
clearTimeout(this.reconnectTimer);
|
||||||
@ -136,29 +132,27 @@ export class WebSocketRpcClient extends s.SprinklersRPC {
|
|||||||
() =>
|
() =>
|
||||||
this.connectionState.clientToServer === true &&
|
this.connectionState.clientToServer === true &&
|
||||||
this.tokenStore.accessToken.isValid,
|
this.tokenStore.accessToken.isValid,
|
||||||
async () => { this.doAuthenticate() }
|
async () => {
|
||||||
|
try {
|
||||||
|
const res = await this.authenticate(
|
||||||
|
this.tokenStore.accessToken.token!
|
||||||
|
);
|
||||||
|
runInAction("authenticateSuccess", () => {
|
||||||
|
this.authenticated = res.authenticated;
|
||||||
|
});
|
||||||
|
logger.info({ user: res.user }, "authenticated websocket connection");
|
||||||
|
this.emit("newUserData", res.user);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "error authenticating websocket connection");
|
||||||
|
// TODO message?
|
||||||
|
runInAction("authenticateError", () => {
|
||||||
|
this.authenticated = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async doAuthenticate() {
|
|
||||||
try {
|
|
||||||
const res = await this.authenticate(
|
|
||||||
this.tokenStore.accessToken.token!
|
|
||||||
);
|
|
||||||
runInAction("authenticateSuccess", () => {
|
|
||||||
this.authenticated = res.authenticated;
|
|
||||||
});
|
|
||||||
logger.info({ user: res.user }, "authenticated websocket connection");
|
|
||||||
this.emit("newUserData", res.user);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error({ err }, "error authenticating websocket connection");
|
|
||||||
// TODO message?
|
|
||||||
runInAction("authenticateError", () => {
|
|
||||||
this.authenticated = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// args must all be JSON serializable
|
// args must all be JSON serializable
|
||||||
async makeDeviceCall(
|
async makeDeviceCall(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
@ -195,16 +189,11 @@ export class WebSocketRpcClient extends s.SprinklersRPC {
|
|||||||
const id = this.nextRequestId++;
|
const id = this.nextRequestId++;
|
||||||
return new Promise<ws.IServerResponseTypes[Method]>((resolve, reject) => {
|
return new Promise<ws.IServerResponseTypes[Method]>((resolve, reject) => {
|
||||||
let timeoutHandle: number;
|
let timeoutHandle: number;
|
||||||
this.responseCallbacks[id] = (response: ws.ServerResponse) => {
|
this.responseCallbacks[id] = response => {
|
||||||
clearTimeout(timeoutHandle);
|
clearTimeout(timeoutHandle);
|
||||||
delete this.responseCallbacks[id];
|
delete this.responseCallbacks[id];
|
||||||
if (response.result === "success") {
|
if (response.result === "success") {
|
||||||
if (response.method === method) {
|
resolve(response.data);
|
||||||
resolve(response.data as ws.IServerResponseTypes[Method]);
|
|
||||||
} else {
|
|
||||||
reject(new s.RpcError("Response method does not match request method", ErrorCode.Internal,
|
|
||||||
{ requestMethod: method, responseMethod: response.method }));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const { error } = response;
|
const { error } = response;
|
||||||
reject(new s.RpcError(error.message, error.code, error.data));
|
reject(new s.RpcError(error.message, error.code, error.data));
|
||||||
|
@ -38,16 +38,8 @@ export default class AppState extends TypedEventEmitter<AppEvents> {
|
|||||||
when(() => !this.tokenStore.accessToken.isValid, this.checkToken);
|
when(() => !this.tokenStore.accessToken.isValid, this.checkToken);
|
||||||
this.sprinklersRpc.start();
|
this.sprinklersRpc.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", this.onPageFocus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageFocus = () => {
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
this.sprinklersRpc.reconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get isLoggedIn() {
|
get isLoggedIn() {
|
||||||
return this.tokenStore.accessToken.isValid;
|
return this.tokenStore.accessToken.isValid;
|
||||||
@ -55,7 +47,7 @@ export default class AppState extends TypedEventEmitter<AppEvents> {
|
|||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
configure({
|
configure({
|
||||||
enforceActions: "observed"
|
enforceActions: true
|
||||||
});
|
});
|
||||||
|
|
||||||
syncHistoryWithStore(this.history, this.routerStore);
|
syncHistoryWithStore(this.history, this.routerStore);
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
import { ISprinklersDevice, IUser } from "@common/httpApi";
|
import { ISprinklersDevice, IUser } from "@common/httpApi";
|
||||||
import { action, IObservableValue, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
|
|
||||||
export class UserStore {
|
export class UserStore {
|
||||||
userData: IObservableValue<IUser | null> = observable.box(null);
|
@observable
|
||||||
|
userData: IUser | null = null;
|
||||||
|
|
||||||
@action.bound
|
@action.bound
|
||||||
receiveUserData(userData: IUser) {
|
receiveUserData(userData: IUser) {
|
||||||
this.userData.set(userData);
|
this.userData = userData;
|
||||||
}
|
|
||||||
|
|
||||||
getUserData(): IUser | null {
|
|
||||||
return this.userData.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findDevice(id: number): ISprinklersDevice | null {
|
findDevice(id: number): ISprinklersDevice | null {
|
||||||
const userData = this.userData.get();
|
|
||||||
return (
|
return (
|
||||||
(userData &&
|
(this.userData &&
|
||||||
userData.devices &&
|
this.userData.devices &&
|
||||||
userData.devices.find(dev => dev.id === id)) ||
|
this.userData.devices.find(dev => dev.id === id)) ||
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -31,18 +31,26 @@ export function ConsumeState({ children }: ConsumeStateProps) {
|
|||||||
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
|
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Diff<
|
||||||
|
T extends string | number | symbol,
|
||||||
|
U extends string | number | symbol
|
||||||
|
> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
|
||||||
|
type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] };
|
||||||
|
|
||||||
export function injectState<P extends { appState: AppState }>(
|
export function injectState<P extends { appState: AppState }>(
|
||||||
Component: React.ComponentType<P>
|
Component: React.ComponentType<P>
|
||||||
): React.FunctionComponent<Omit<P, "appState">> {
|
): React.ComponentClass<Omit<P, "appState">> {
|
||||||
return function InjectState(props) {
|
return class extends React.Component<Omit<P, "appState">> {
|
||||||
const state = React.useContext(StateContext);
|
render() {
|
||||||
if (state == null) {
|
const consumeState = (state: AppState | null) => {
|
||||||
throw new Error(
|
if (state == null) {
|
||||||
"Component with injectState must be mounted inside ProvideState"
|
throw new Error(
|
||||||
);
|
"Component with injectState must be mounted inside ProvideState"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Component {...this.props} appState={state} />;
|
||||||
|
};
|
||||||
|
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
|
||||||
}
|
}
|
||||||
// tslint:disable-next-line: no-object-literal-type-assertion
|
};
|
||||||
const allProps: Readonly<P> = {...props, appState: state} as Readonly<P>;
|
|
||||||
return <Component {...allProps} />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,3 @@
|
|||||||
.devices-header {
|
|
||||||
display: flex;
|
|
||||||
.ui.icon.button {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$disconnected-color: #d20000;
|
|
||||||
$connected-color: #13d213;
|
|
||||||
|
|
||||||
.device {
|
.device {
|
||||||
.header {
|
.header {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
@ -16,29 +6,6 @@ $connected-color: #13d213;
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-body {
|
|
||||||
position: relative;
|
|
||||||
margin: 0em -1em 0em -1em;
|
|
||||||
padding: 0em 1em 1em 1em;
|
|
||||||
|
|
||||||
&.blurring.dimmable.dimmed {
|
|
||||||
// border: $disconnected-color 1px solid;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
|
|
||||||
> :not(.dimmer) {
|
|
||||||
-webkit-filter: blur(1px) grayscale(0.7);
|
|
||||||
// filter: blur(1px) grayscale(0.7);
|
|
||||||
filter: grayscale(0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui.dimmer {
|
|
||||||
// border-radius: 1em;
|
|
||||||
background-color: adjust-color($disconnected-color, $alpha: -0.95);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui.grid {
|
.ui.grid {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
@ -50,11 +17,11 @@ $connected-color: #13d213;
|
|||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
|
|
||||||
&.connected {
|
&.connected {
|
||||||
color: $connected-color;
|
color: #13d213;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disconnected {
|
&.disconnected {
|
||||||
color: $disconnected-color;
|
color: #d20000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,24 +45,6 @@ $connected-color: #13d213;
|
|||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.program--nextRun {
|
.ui.modal.programEditor > .header > .header.item .inline.fields {
|
||||||
display: inline-block;
|
margin-bottom: 0;
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui.modal.programEditor {
|
|
||||||
&.editing > .content {
|
|
||||||
min-height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .header > .header.item .inline.fields {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runSectionForm-runButton {
|
|
||||||
display: inline-block;
|
|
||||||
&, .ui.disabled.button {
|
|
||||||
pointer-events: auto !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ $durationInput-labelWidth: 2.5em;
|
|||||||
|
|
||||||
.field .durationInputs {
|
.field .durationInputs {
|
||||||
display: flex; // max-width: 100%;
|
display: flex; // max-width: 100%;
|
||||||
justify-content: flex-start;
|
justify-content: start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: -$durationInput-spacing / 2;
|
margin: -$durationInput-spacing / 2;
|
||||||
|
|
||||||
|
@ -8,16 +8,12 @@
|
|||||||
.programSequence-item {
|
.programSequence-item {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
&.dragging {
|
&.dragging {
|
||||||
z-index: 1010;
|
z-index: 1010;
|
||||||
}
|
}
|
||||||
.fields {
|
.fields {
|
||||||
display: flex;
|
margin: 0em 0em 1em !important;
|
||||||
align-items: center;
|
|
||||||
margin: 0em !important;
|
|
||||||
padding: 0em !important;
|
|
||||||
}
|
}
|
||||||
.ui.icon.button {
|
.ui.icon.button {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
"webpack-env",
|
"webpack-env",
|
||||||
// "core-js",
|
"core-js",
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
"baseUrl": "..",
|
"baseUrl": "..",
|
||||||
@ -24,10 +24,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*.ts",
|
"./client/**/*.ts",
|
||||||
"./**/*.tsx"
|
"./client/**/*.tsx"
|
||||||
],
|
],
|
||||||
"exclude": [],
|
|
||||||
"references": [{
|
"references": [{
|
||||||
"path": "../common"
|
"path": "../common"
|
||||||
}]
|
}]
|
||||||
|
@ -2,9 +2,9 @@ const path = require("path");
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
|
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
// const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||||
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
|
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
|
||||||
const DashboardPlugin = require("webpack-dashboard/plugin");
|
const DashboardPlugin = require("webpack-dashboard/plugin");
|
||||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||||
.BundleAnalyzerPlugin;
|
.BundleAnalyzerPlugin;
|
||||||
@ -110,7 +110,6 @@ function getConfig(env) {
|
|||||||
},
|
},
|
||||||
cssRule,
|
cssRule,
|
||||||
sassRule,
|
sassRule,
|
||||||
// Process TypeScript with TSC through HappyPack.
|
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
include: [paths.clientDir, paths.commonDir],
|
include: [paths.clientDir, paths.commonDir],
|
||||||
@ -126,7 +125,7 @@ function getConfig(env) {
|
|||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
options: {
|
options: {
|
||||||
configFile: paths.clientTsConfig,
|
configFile: paths.clientTsConfig,
|
||||||
happyPackMode: true
|
transpileOnly: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -170,17 +169,21 @@ function getConfig(env) {
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}),
|
}),
|
||||||
// new FaviconsWebpackPlugin({
|
new FaviconsWebpackPlugin({
|
||||||
// logo: path.resolve(paths.clientDir, "images", "favicon-96x96.png"),
|
logo: path.resolve(paths.clientDir, "images", "favicon-96x96.png"),
|
||||||
// emitStatis: false,
|
emitStatis: false,
|
||||||
// prefix: "static/icons-[hash]/"
|
prefix: "static/icons-[hash]/"
|
||||||
// }),
|
}),
|
||||||
// Makes some environment variables available to the JS code, for example:
|
// Makes some environment variables available to the JS code, for example:
|
||||||
// if (process.env.NODE_ENV === "production") { ... }. See `./env.js`.
|
// if (process.env.NODE_ENV === "production") { ... }. See `./env.js`.
|
||||||
// It is absolutely essential that NODE_ENV was set to production here.
|
// It is absolutely essential that NODE_ENV was set to production here.
|
||||||
// Otherwise React will be compiled in the very slow development mode.
|
// Otherwise React will be compiled in the very slow development mode.
|
||||||
new webpack.DefinePlugin(environ.stringified),
|
new webpack.DefinePlugin(environ.stringified),
|
||||||
new CaseSensitivePathsPlugin(),
|
new CaseSensitivePathsPlugin(),
|
||||||
|
isProd &&
|
||||||
|
new UglifyJsPlugin({
|
||||||
|
sourceMap: shouldUseSourceMap
|
||||||
|
}),
|
||||||
isDev && new webpack.HotModuleReplacementPlugin(),
|
isDev && new webpack.HotModuleReplacementPlugin(),
|
||||||
new ForkTsCheckerWebpackPlugin({
|
new ForkTsCheckerWebpackPlugin({
|
||||||
checkSyntacticErrors: true,
|
checkSyntacticErrors: true,
|
||||||
@ -235,17 +238,13 @@ function getConfig(env) {
|
|||||||
extensions: [".ts", ".tsx", ".js", ".json", ".scss"],
|
extensions: [".ts", ".tsx", ".js", ".json", ".scss"],
|
||||||
alias: {
|
alias: {
|
||||||
"@client": paths.clientDir,
|
"@client": paths.clientDir,
|
||||||
"@common": paths.commonDir,
|
"@common": paths.commonDir
|
||||||
"react-dom": isDev ? "@hot-loader/react-dom" : "react-dom"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
module: { rules },
|
module: { rules },
|
||||||
plugins: plugins,
|
plugins: plugins,
|
||||||
optimization: {
|
optimization: {
|
||||||
namedModules: isProd,
|
namedModules: isProd
|
||||||
minimizer: isProd ? [new TerserPlugin({
|
|
||||||
sourceMap: shouldUseSourceMap
|
|
||||||
})] : [],
|
|
||||||
},
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
hot: true,
|
hot: true,
|
||||||
@ -258,7 +257,7 @@ function getConfig(env) {
|
|||||||
target: paths.publicUrl
|
target: paths.publicUrl
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,10 +7,9 @@ export enum ErrorCode {
|
|||||||
BadToken = 105,
|
BadToken = 105,
|
||||||
Unauthorized = 106,
|
Unauthorized = 106,
|
||||||
NoPermission = 107,
|
NoPermission = 107,
|
||||||
|
NotImplemented = 108,
|
||||||
NotFound = 109,
|
NotFound = 109,
|
||||||
NotUnique = 110,
|
|
||||||
Internal = 200,
|
Internal = 200,
|
||||||
NotImplemented = 201,
|
|
||||||
Timeout = 300,
|
Timeout = 300,
|
||||||
ServerDisconnected = 301,
|
ServerDisconnected = 301,
|
||||||
BrokerDisconnected = 302
|
BrokerDisconnected = 302
|
||||||
@ -23,7 +22,6 @@ export function toHttpStatus(errorCode: ErrorCode): number {
|
|||||||
case ErrorCode.Parse:
|
case ErrorCode.Parse:
|
||||||
case ErrorCode.Range:
|
case ErrorCode.Range:
|
||||||
case ErrorCode.InvalidData:
|
case ErrorCode.InvalidData:
|
||||||
case ErrorCode.NotUnique:
|
|
||||||
return 400; // Bad request
|
return 400; // Bad request
|
||||||
case ErrorCode.Unauthorized:
|
case ErrorCode.Unauthorized:
|
||||||
case ErrorCode.BadToken:
|
case ErrorCode.BadToken:
|
||||||
|
@ -133,7 +133,7 @@ export type IResponseHandler<
|
|||||||
ResponseTypes,
|
ResponseTypes,
|
||||||
ErrorType,
|
ErrorType,
|
||||||
Method extends keyof ResponseTypes = keyof ResponseTypes
|
Method extends keyof ResponseTypes = keyof ResponseTypes
|
||||||
> = (response: Response<ResponseTypes, ErrorType, Method>) => void;
|
> = (response: ResponseData<ResponseTypes, ErrorType, Method>) => void;
|
||||||
|
|
||||||
export interface ResponseHandlers<
|
export interface ResponseHandlers<
|
||||||
ResponseTypes = DefaultResponseTypes,
|
ResponseTypes = DefaultResponseTypes,
|
||||||
|
@ -32,8 +32,6 @@ export class Program {
|
|||||||
sequence: ProgramItem[] = [];
|
sequence: ProgramItem[] = [];
|
||||||
@observable
|
@observable
|
||||||
running: boolean = false;
|
running: boolean = false;
|
||||||
@observable
|
|
||||||
nextRun: Date | null = null;
|
|
||||||
|
|
||||||
constructor(device: SprinklersDevice, id: number, data?: Partial<Program>) {
|
constructor(device: SprinklersDevice, id: number, data?: Partial<Program>) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
@ -62,8 +60,7 @@ export class Program {
|
|||||||
enabled: this.enabled,
|
enabled: this.enabled,
|
||||||
running: this.running,
|
running: this.running,
|
||||||
schedule: this.schedule.clone(),
|
schedule: this.schedule.clone(),
|
||||||
sequence: this.sequence.slice(),
|
sequence: this.sequence.slice()
|
||||||
nextRun: this.nextRun,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +68,7 @@ export class Program {
|
|||||||
return (
|
return (
|
||||||
`Program{name="${this.name}", enabled=${this.enabled}, schedule=${
|
`Program{name="${this.name}", enabled=${this.enabled}, schedule=${
|
||||||
this.schedule
|
this.schedule
|
||||||
}, ` + `sequence=${this.sequence}, running=${this.running}, nextRun=${this.nextRun}}`
|
}, ` + `sequence=${this.sequence}, running=${this.running}}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,6 @@ export class MqttProgram extends s.Program {
|
|||||||
onMessage(payload: string, topic: string | undefined) {
|
onMessage(payload: string, topic: string | undefined) {
|
||||||
if (topic === "running") {
|
if (topic === "running") {
|
||||||
this.running = payload === "true";
|
this.running = payload === "true";
|
||||||
} else if (topic === "nextRun") {
|
|
||||||
this.nextRun = (payload.length > 0) ? new Date(Number(payload) * 1000.0) : null;
|
|
||||||
} else if (topic == null) {
|
} else if (topic == null) {
|
||||||
this.updateFromJSON(JSON.parse(payload));
|
this.updateFromJSON(JSON.parse(payload));
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ class MqttSprinklersDevice extends s.SprinklersDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
doUnsubscribe() {
|
doUnsubscribe() {
|
||||||
this.apiClient.client.unsubscribe(this.subscriptions, (err: Error | undefined) => {
|
this.apiClient.client.unsubscribe(this.subscriptions, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error({ err, id: this.id }, "error unsubscribing to device");
|
log.error({ err, id: this.id }, "error unsubscribing to device");
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
import {Context, custom, ModelSchema, primitive, PropSchema} from 'serializr';
|
import { ModelSchema, primitive, PropSchema } from "serializr";
|
||||||
|
import * as s from "..";
|
||||||
import * as s from '..';
|
|
||||||
|
|
||||||
export const duration: PropSchema = primitive();
|
export const duration: PropSchema = primitive();
|
||||||
|
|
||||||
export const date: PropSchema = custom(
|
export const date: PropSchema = {
|
||||||
(jsDate: Date|null) => jsDate != null ? jsDate.toISOString() : null,
|
serializer: (jsDate: Date | null) =>
|
||||||
(json: any, context: Context, oldValue: any,
|
jsDate != null ? jsDate.toISOString() : null,
|
||||||
done: (err: any, value: any) => void) => {
|
deserializer: (json: any, done) => {
|
||||||
if (json === null) {
|
if (json === null) {
|
||||||
return done(null, null);
|
return done(null, null);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
done(null, new Date(json));
|
done(null, new Date(json));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
done(e, undefined);
|
done(e, undefined);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const dateOfYear: ModelSchema<s.DateOfYear> = {
|
export const dateOfYear: ModelSchema<s.DateOfYear> = {
|
||||||
factory: () => new s.DateOfYear(),
|
factory: () => new s.DateOfYear(),
|
||||||
props: {
|
props: {
|
||||||
year: primitive(),
|
year: primitive(),
|
||||||
month:
|
month: primitive(), // this only works if it is represented as a # from 0-12
|
||||||
primitive(), // this only works if it is represented as a # from 0-12
|
|
||||||
day: primitive()
|
day: primitive()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createSimpleSchema, ModelSchema, object, primitive, date } from "serializr";
|
import { createSimpleSchema, ModelSchema, object, primitive } from "serializr";
|
||||||
import * as s from "..";
|
import * as s from "..";
|
||||||
import list from "./list";
|
import list from "./list";
|
||||||
|
|
||||||
@ -85,8 +85,7 @@ export const program: ModelSchema<s.Program> = {
|
|||||||
enabled: primitive(),
|
enabled: primitive(),
|
||||||
schedule: object(schedule),
|
schedule: object(schedule),
|
||||||
sequence: list(object(programItem)),
|
sequence: list(object(programItem)),
|
||||||
running: primitive(),
|
running: primitive()
|
||||||
nextRun: date(),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {primitive, PropSchema} from 'serializr';
|
import { primitive, PropSchema } from "serializr";
|
||||||
|
|
||||||
function invariant(cond: boolean, message?: string) {
|
function invariant(cond: boolean, message?: string) {
|
||||||
if (!cond) {
|
if (!cond) {
|
||||||
throw new Error('[serializr] ' + (message || 'Illegal ServerState'));
|
throw new Error("[serializr] " + (message || "Illegal ServerState"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,11 +11,14 @@ function isPropSchema(thing: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isAliasedPropSchema(propSchema: any) {
|
function isAliasedPropSchema(propSchema: any) {
|
||||||
return typeof propSchema === 'object' && !!propSchema.jsonname;
|
return typeof propSchema === "object" && !!propSchema.jsonname;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parallel(
|
function parallel(
|
||||||
ar: any[], processor: (item: any, done: any) => void, cb: any) {
|
ar: any[],
|
||||||
|
processor: (item: any, done: any) => void,
|
||||||
|
cb: any
|
||||||
|
) {
|
||||||
if (ar.length === 0) {
|
if (ar.length === 0) {
|
||||||
return void cb(null, []);
|
return void cb(null, []);
|
||||||
}
|
}
|
||||||
@ -40,15 +43,17 @@ function parallel(
|
|||||||
|
|
||||||
export default function list(propSchema: PropSchema): PropSchema {
|
export default function list(propSchema: PropSchema): PropSchema {
|
||||||
propSchema = propSchema || primitive();
|
propSchema = propSchema || primitive();
|
||||||
invariant(isPropSchema(propSchema), 'expected prop schema as first argument');
|
invariant(isPropSchema(propSchema), "expected prop schema as first argument");
|
||||||
invariant(
|
invariant(
|
||||||
!isAliasedPropSchema(propSchema),
|
!isAliasedPropSchema(propSchema),
|
||||||
'provided prop is aliased, please put aliases first');
|
"provided prop is aliased, please put aliases first"
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
serializer(ar) {
|
serializer(ar) {
|
||||||
invariant(
|
invariant(
|
||||||
ar && typeof ar.length === 'number' && typeof ar.map === 'function',
|
ar && typeof ar.length === "number" && typeof ar.map === "function",
|
||||||
'expected array (like) object');
|
"expected array (like) object"
|
||||||
|
);
|
||||||
return ar.map(propSchema.serializer);
|
return ar.map(propSchema.serializer);
|
||||||
},
|
},
|
||||||
deserializer(jsonArray, done, context) {
|
deserializer(jsonArray, done, context) {
|
||||||
@ -57,15 +62,14 @@ export default function list(propSchema: PropSchema): PropSchema {
|
|||||||
return void done(null, []);
|
return void done(null, []);
|
||||||
}
|
}
|
||||||
if (!Array.isArray(jsonArray)) {
|
if (!Array.isArray(jsonArray)) {
|
||||||
return void done('[serializr] expected JSON array', null);
|
return void done("[serializr] expected JSON array", null);
|
||||||
}
|
}
|
||||||
parallel(
|
parallel(
|
||||||
jsonArray,
|
jsonArray,
|
||||||
(item: any, itemDone: (err: any, targetPropertyValue: any) => void) =>
|
(item: any, itemDone: (err: any, targetPropertyValue: any) => void) =>
|
||||||
propSchema.deserializer(item, itemDone, context, undefined),
|
propSchema.deserializer(item, itemDone, context, undefined),
|
||||||
done);
|
done
|
||||||
},
|
);
|
||||||
beforeDeserialize: undefined as any,
|
}
|
||||||
afterDeserialize: undefined as any,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -35,9 +35,7 @@ export const updateProgram: ModelSchema<
|
|||||||
serializer: data => data,
|
serializer: data => data,
|
||||||
deserializer: (json, done) => {
|
deserializer: (json, done) => {
|
||||||
done(null, json);
|
done(null, json);
|
||||||
},
|
}
|
||||||
beforeDeserialize: undefined as any,
|
|
||||||
afterDeserialize: undefined as any,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*.ts",
|
"./**/*.ts"
|
||||||
],
|
]
|
||||||
"exclude": []
|
|
||||||
}
|
}
|
@ -12,7 +12,6 @@ services:
|
|||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
- "8081:8081"
|
- "8081:8081"
|
||||||
volumes:
|
volumes:
|
||||||
- ./bin:/app/bin
|
|
||||||
- ./client:/app/client
|
- ./client:/app/client
|
||||||
- ./common:/app/common
|
- ./common:/app/common
|
||||||
- ./server:/app/server
|
- ./server:/app/server
|
||||||
@ -37,13 +36,8 @@ services:
|
|||||||
- "1883:1883"
|
- "1883:1883"
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: "postgres:11"
|
image: "postgres:11-alpine"
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
|
||||||
- data-volume:/var/lib/postgres/data
|
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
- POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
||||||
|
|
||||||
volumes:
|
|
||||||
data-volume:
|
|
@ -20,6 +20,6 @@ services:
|
|||||||
# Must specify JWT_SECRET and MQTT_URL
|
# Must specify JWT_SECRET and MQTT_URL
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: "postgres:11"
|
image: "postgres:11-alpine"
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
- POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
||||||
|
146
package.json
146
package.json
@ -43,111 +43,115 @@
|
|||||||
"commands": "./dist/commands"
|
"commands": "./dist/commands"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oclif/command": "^1.5.0",
|
"@oclif/command": "^1.5.6",
|
||||||
"@oclif/config": "^1.7.4",
|
"@oclif/config": "^1.9.0",
|
||||||
"@oclif/plugin-help": "^2.1.1",
|
"@oclif/plugin-help": "^2.1.4",
|
||||||
"bcrypt": "^3.0.0",
|
"@types/split2": "^2.1.6",
|
||||||
|
"bcrypt": "^3.0.2",
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
"chalk": "^2.4.1",
|
"chalk": "^2.4.1",
|
||||||
"cli-ux": "^5.3.1",
|
"cli-ux": "^4.9.3",
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.4",
|
||||||
|
"express-pino-logger": "^4.0.0",
|
||||||
"express-promise-router": "^3.0.3",
|
"express-promise-router": "^3.0.3",
|
||||||
"globby": "^10.0.1",
|
"jsonwebtoken": "^8.4.0",
|
||||||
"jsonwebtoken": "^8.3.0",
|
"lodash": "^4.17.11",
|
||||||
"lodash": "^4.17.10",
|
"mobx": "^5.7.0",
|
||||||
"mobx": "^5.1.0",
|
"mobx-utils": "^5.1.0",
|
||||||
"mobx-utils": "^5.0.1",
|
|
||||||
"module-alias": "^2.1.0",
|
"module-alias": "^2.1.0",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"mqtt": "^3.0.0",
|
"mqtt": "^2.18.8",
|
||||||
"pg": "^7.4.3",
|
"pg": "^7.7.1",
|
||||||
"pino": "^5.4.0",
|
"pino": "^5.10.0",
|
||||||
"pino-http": "^4.2.0",
|
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
"reflect-metadata": "^0.1.12",
|
"reflect-metadata": "^0.1.12",
|
||||||
"serializr": "^1.3.0",
|
"serializr": "^1.3.0",
|
||||||
"split2": "^3.0.0",
|
"split2": "^3.0.0",
|
||||||
"terser-webpack-plugin": "^1.3.0",
|
"through2": "^3.0.0",
|
||||||
"through2": "^3.0.1",
|
"typeorm": "^0.2.9",
|
||||||
"typeorm": "^0.2.7",
|
"ws": "^6.1.2"
|
||||||
"ws": "^7.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hot-loader/react-dom": "^16.8.6",
|
"@types/async": "^2.0.50",
|
||||||
"@types/async": "^3.0.0",
|
|
||||||
"@types/bcrypt": "^3.0.0",
|
"@types/bcrypt": "^3.0.0",
|
||||||
"@types/classnames": "^2.2.6",
|
"@types/classnames": "^2.2.6",
|
||||||
"@types/core-js": "^2.5.0",
|
"@types/core-js": "^2.5.0",
|
||||||
"@types/express": "^4.16.0",
|
"@types/express": "^4.16.0",
|
||||||
"@types/jsonwebtoken": "^8.3.2",
|
"@types/jsonwebtoken": "^8.3.0",
|
||||||
"@types/lodash": "^4.14.116",
|
"@types/lodash": "^4.14.119",
|
||||||
"@types/module-alias": "^2.0.0",
|
"@types/module-alias": "^2.0.0",
|
||||||
"@types/node": "^11.11.3",
|
"@types/node": "^10.12.12",
|
||||||
"@types/object-assign": "^4.0.30",
|
"@types/object-assign": "^4.0.30",
|
||||||
"@types/pino": "^5.20.0",
|
"@types/pino": "^5.20.0",
|
||||||
"@types/pino-http": "^4.0.2",
|
"@types/prop-types": "^15.5.7",
|
||||||
"@types/prop-types": "^15.5.5",
|
|
||||||
"@types/pump": "^1.0.1",
|
"@types/pump": "^1.0.1",
|
||||||
"@types/query-string": "^6.1.0",
|
"@types/query-string": "^6.1.1",
|
||||||
"@types/react": "^16.7.13",
|
"@types/react": "16.7.13",
|
||||||
"@types/react-dom": "^16.0.11",
|
"@types/react-dom": "16.0.11",
|
||||||
"@types/react-hot-loader": "^4.1.0",
|
"@types/react-hot-loader": "^4.1.0",
|
||||||
"@types/react-router-dom": "^4.3.0",
|
"@types/react-router-dom": "^4.3.1",
|
||||||
"@types/react-sortable-hoc": "^0.6.4",
|
"@types/react-sortable-hoc": "^0.6.4",
|
||||||
"@types/split2": "^2.1.6",
|
"@types/through2": "^2.0.34",
|
||||||
"@types/through2": "^2.0.33",
|
|
||||||
"@types/webpack-env": "^1.13.6",
|
"@types/webpack-env": "^1.13.6",
|
||||||
"@types/ws": "^6.0.0",
|
"@types/ws": "^6.0.1",
|
||||||
"async": "^3.1.0",
|
"async": "^2.6.1",
|
||||||
"autoprefixer": "^9.1.3",
|
"autoprefixer": "^9.4.2",
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^1.2.5",
|
||||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"css-loader": "^3.1.0",
|
"css-loader": "^2.0.0",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^6.2.0",
|
||||||
"favicons-webpack-plugin": "^0.0.9",
|
"favicons-webpack-plugin": "^0.0.9",
|
||||||
"file-loader": "^4.1.0",
|
"file-loader": "^2.0.0",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^1.4.3",
|
"fork-ts-checker-webpack-plugin": "^0.5.1",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"mini-css-extract-plugin": "^0.8.0",
|
"mini-css-extract-plugin": "^0.5.0",
|
||||||
"mobx-react": "^6.1.1",
|
"mobx-react": "^5.4.2",
|
||||||
"mobx-react-devtools": "^6.0.3",
|
"mobx-react-devtools": "^6.0.3",
|
||||||
"mobx-react-router": "^4.0.4",
|
"mobx-react-router": "^4.0.5",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.11.0",
|
||||||
"nodemon": "^1.18.4",
|
"nodemon": "^1.18.8",
|
||||||
"npm-run-all": "^4.1.3",
|
"npm-run-all": "^4.1.5",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"postcss-flexbugs-fixes": "^4.1.0",
|
"postcss-flexbugs-fixes": "^4.1.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.4.0",
|
||||||
"promise": "^8.0.1",
|
"promise": "^8.0.2",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"query-string": "^6.1.0",
|
"query-string": "^6.2.0",
|
||||||
"react": "^16.8.0",
|
"react": "16.6.3",
|
||||||
"react-dev-utils": "^9.0.1",
|
"react-dev-utils": "^6.1.1",
|
||||||
"react-dom": "^16.6.3",
|
"react-dom": "16.6.3",
|
||||||
"react-hot-loader": "^4.3.5",
|
"react-hot-loader": "^4.3.12",
|
||||||
"react-router": "^5.0.1",
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^5.0.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-sortable-hoc": "^1.9.1",
|
"react-sortable-hoc": "^0.8.4",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"semantic-ui-css": "^2.3.3",
|
"semantic-ui-css": "^2.4.1",
|
||||||
"semantic-ui-react": "^0.87.3",
|
"semantic-ui-react": "^0.84.0",
|
||||||
"source-map-loader": "^0.2.4",
|
"source-map-loader": "^0.2.4",
|
||||||
"style-loader": "^0.23.0",
|
"style-loader": "^0.23.1",
|
||||||
"thread-loader": "^2.1.2",
|
"thread-loader": "^1.2.0",
|
||||||
"ts-loader": "^6.0.4",
|
"ts-loader": "^5.3.1",
|
||||||
"tslint": "^5.11.0",
|
"tslint": "^5.11.0",
|
||||||
"tslint-config-prettier": "^1.15.0",
|
"tslint-config-prettier": "^1.17.0",
|
||||||
"tslint-consistent-codestyle": "^1.13.3",
|
"tslint-consistent-codestyle": "^1.14.1",
|
||||||
"tslint-react": "^4.0.0",
|
"tslint-react": "^3.6.0",
|
||||||
"typescript": "^3.0.3",
|
"typescript": "^3.2.2",
|
||||||
"url-loader": "^2.1.0",
|
"uglify-es": "^3.3.9",
|
||||||
"webpack": "^4.17.1",
|
"uglifyjs-webpack-plugin": "^2.0.1",
|
||||||
"webpack-bundle-analyzer": "^3.3.2",
|
"url-loader": "^1.1.2",
|
||||||
"webpack-cli": "^3.1.0",
|
"webpack": "^4.27.1",
|
||||||
"webpack-dashboard": "^3.0.7",
|
"webpack-bundle-analyzer": "^3.0.3",
|
||||||
"webpack-dev-server": "^3.1.7"
|
"webpack-cli": "^3.1.2",
|
||||||
|
"webpack-dashboard": "^2.0.0",
|
||||||
|
"webpack-dev-server": "^3.1.10"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"**/@types/react": "16.7.13",
|
||||||
|
"**/@types/react-dom": "16.0.11",
|
||||||
|
"**/react": "16.6.3",
|
||||||
|
"**/react-dom": "16.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Connection, createConnection, getConnectionOptions } from "typeorm";
|
|||||||
|
|
||||||
import logger from "@common/logger";
|
import logger from "@common/logger";
|
||||||
|
|
||||||
import { SprinklersDevice, User } from "./entities";
|
import { User } from "./entities";
|
||||||
import { SprinklersDeviceRepository, UserRepository } from "./repositories/";
|
import { SprinklersDeviceRepository, UserRepository } from "./repositories/";
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
@ -24,30 +24,25 @@ export class Database {
|
|||||||
Object.assign(options, {
|
Object.assign(options, {
|
||||||
entities: [path.resolve(__dirname, "entities", "*.js")]
|
entities: [path.resolve(__dirname, "entities", "*.js")]
|
||||||
});
|
});
|
||||||
if (options.synchronize) {
|
|
||||||
logger.warn("synchronizing database schema");
|
|
||||||
}
|
|
||||||
this._conn = await createConnection(options);
|
this._conn = await createConnection(options);
|
||||||
this.users = this._conn.getCustomRepository(UserRepository);
|
this.users = this._conn.getCustomRepository(UserRepository);
|
||||||
this.sprinklersDevices = this._conn.getCustomRepository(
|
this.sprinklersDevices = this._conn.getCustomRepository(
|
||||||
SprinklersDeviceRepository
|
SprinklersDeviceRepository
|
||||||
);
|
);
|
||||||
logger.info("connected to database");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
if (this._conn) {
|
if (this._conn) {
|
||||||
await this._conn.close();
|
return this._conn.close();
|
||||||
logger.info("disconnected from database");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertTestData() {
|
async insertTestData() {
|
||||||
const NUM = 50;
|
const NUM = 100;
|
||||||
const users: User[] = [];
|
const users: User[] = [];
|
||||||
for (let i = 0; i < NUM; i++) {
|
for (let i = 0; i < NUM; i++) {
|
||||||
const username = "alex" + i;
|
const username = "alex" + i;
|
||||||
let user = await this.users.findByUsername(username, { devices: true });
|
let user = await this.users.findByUsername(username);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await this.users.create({
|
user = await this.users.create({
|
||||||
name: "Alex Mikhalev" + i,
|
name: "Alex Mikhalev" + i,
|
||||||
@ -58,8 +53,6 @@ export class Database {
|
|||||||
users.push(user);
|
users.push(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices: SprinklersDevice[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < NUM; i++) {
|
for (let i = 0; i < NUM; i++) {
|
||||||
const name = "Test" + i;
|
const name = "Test" + i;
|
||||||
let device = await this.sprinklersDevices.findByName(name);
|
let device = await this.sprinklersDevices.findByName(name);
|
||||||
@ -70,17 +63,13 @@ export class Database {
|
|||||||
name,
|
name,
|
||||||
deviceId: "grinklers" + (i === 1 ? "" : i)
|
deviceId: "grinklers" + (i === 1 ? "" : i)
|
||||||
});
|
});
|
||||||
devices.push(device);
|
await this.sprinklersDevices.save(device);
|
||||||
for (let j = 0; j < 5; j++) {
|
for (let j = 0; j < 5; j++) {
|
||||||
const userIdx = (i + j * 10) % NUM;
|
const userIdx = (i + j * 10) % NUM;
|
||||||
const user = users[userIdx];
|
const user = users[userIdx];
|
||||||
if (!user.devices) {
|
user.devices = (user.devices || []).concat([device]);
|
||||||
user.devices = [];
|
|
||||||
}
|
|
||||||
user.devices.push(device);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.sprinklersDevices.save(devices);
|
|
||||||
logger.info("inserted/updated devices");
|
logger.info("inserted/updated devices");
|
||||||
|
|
||||||
await this.users.save(users);
|
await this.users.save(users);
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import Command from "@oclif/command";
|
|
||||||
|
|
||||||
import { Database, ServerState } from ".";
|
|
||||||
|
|
||||||
export default abstract class ManageCommand extends Command {
|
|
||||||
state!: ServerState;
|
|
||||||
database!: Database;
|
|
||||||
|
|
||||||
async connect() {
|
|
||||||
this.state = new ServerState();
|
|
||||||
await this.state.startDatabase();
|
|
||||||
this.database = this.state.database;
|
|
||||||
}
|
|
||||||
|
|
||||||
async finally(e: Error | undefined) {
|
|
||||||
if (this.state) {
|
|
||||||
await this.state.stopDatabase();
|
|
||||||
}
|
|
||||||
await super.finally(e);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import { QueryFailedError } from "typeorm";
|
|
||||||
|
|
||||||
import ApiError from "@common/ApiError";
|
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
|
||||||
|
|
||||||
export default class UniqueConstraintError extends ApiError {
|
|
||||||
static is(err: any): err is QueryFailedError {
|
|
||||||
return err && err.name === "QueryFailedError" && (err as any).code === 23505; // unique constraint error
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(err: QueryFailedError) {
|
|
||||||
super(`Unsatisfied unique constraint: ${(err as any).detail}`, ErrorCode.NotUnique, err);
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,7 +22,7 @@ if (!JWT_SECRET) {
|
|||||||
const ISSUER = "sprinklers3";
|
const ISSUER = "sprinklers3";
|
||||||
|
|
||||||
const ACCESS_TOKEN_LIFETIME = 30 * 60; // 30 minutes
|
const ACCESS_TOKEN_LIFETIME = 30 * 60; // 30 minutes
|
||||||
const REFRESH_TOKEN_LIFETIME = 7 * 24 * 60 * 60; // 7 days
|
const REFRESH_TOKEN_LIFETIME = 24 * 60 * 60; // 24 hours
|
||||||
|
|
||||||
function signToken(
|
function signToken(
|
||||||
claims: tok.TokenClaimTypes,
|
claims: tok.TokenClaimTypes,
|
||||||
|
@ -1,161 +0,0 @@
|
|||||||
import { flags } from "@oclif/command";
|
|
||||||
import { ux } from "cli-ux";
|
|
||||||
import { capitalize } from "lodash";
|
|
||||||
import { FindConditions } from "typeorm";
|
|
||||||
|
|
||||||
import ManageCommand from "../ManageCommand";
|
|
||||||
|
|
||||||
import { Input } from "@oclif/parser/lib/flags";
|
|
||||||
import { SprinklersDevice, User } from "@server/entities";
|
|
||||||
|
|
||||||
type DeviceFlags = (typeof DeviceCommand)["flags"] extends Input<infer F>
|
|
||||||
? F
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type Action = "show" | "delete" | "add-user" | "remove-user";
|
|
||||||
|
|
||||||
const VALID_ACTIONS: Action[] = [ "show", "delete", "add-user", "remove-user" ];
|
|
||||||
|
|
||||||
// tslint:disable:no-shadowed-variable
|
|
||||||
|
|
||||||
export default class DeviceCommand extends ManageCommand {
|
|
||||||
static description = "Manage devices";
|
|
||||||
|
|
||||||
static flags = {
|
|
||||||
show: flags.boolean({
|
|
||||||
char: "s",
|
|
||||||
exclusive: ["add-user", "delete"],
|
|
||||||
description: "Show devices(s)",
|
|
||||||
}),
|
|
||||||
"add-user": flags.boolean({
|
|
||||||
char: "a",
|
|
||||||
exclusive: ["show", "delete"],
|
|
||||||
description: "Add a user as owning this device (specify --username)"
|
|
||||||
}),
|
|
||||||
"remove-user": flags.boolean({
|
|
||||||
char: "r",
|
|
||||||
exclusive: ["add-user", "show", "delete"],
|
|
||||||
description: "Remove a user as owning this device (specify --username)"
|
|
||||||
}),
|
|
||||||
delete: flags.boolean({
|
|
||||||
char: "d",
|
|
||||||
exclusive: ["show", "add-user"],
|
|
||||||
description: "Delete a user (by --id or --username)"
|
|
||||||
}),
|
|
||||||
id: flags.integer({
|
|
||||||
description: "The id of the device to update or delete",
|
|
||||||
}),
|
|
||||||
name: flags.string({
|
|
||||||
description: "The name of the device, when creating or updating"
|
|
||||||
}),
|
|
||||||
deviceId: flags.string({
|
|
||||||
description: "The deviceId of the device, when creating or updating"
|
|
||||||
}),
|
|
||||||
username: flags.string({
|
|
||||||
description: "Specify a username for --show or --add-user"
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
getAction(flags: DeviceFlags): Action {
|
|
||||||
for (const action of VALID_ACTIONS) {
|
|
||||||
if (flags[action]) {
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const actionFlags = VALID_ACTIONS.map(action => `--${action}`);
|
|
||||||
this.error(`Must specify an action (${actionFlags.join(', ')})`, {
|
|
||||||
exit: false,
|
|
||||||
});
|
|
||||||
return this._help();
|
|
||||||
}
|
|
||||||
|
|
||||||
getFindConditions(flags: DeviceFlags, action: Action): FindConditions<SprinklersDevice> {
|
|
||||||
const whereClause: FindConditions<SprinklersDevice> = {};
|
|
||||||
if (flags.id) {
|
|
||||||
whereClause.id = flags.id;
|
|
||||||
}
|
|
||||||
if (flags.name) {
|
|
||||||
whereClause.name = flags.name;
|
|
||||||
}
|
|
||||||
if (flags.deviceId) {
|
|
||||||
whereClause.deviceId = flags.deviceId;
|
|
||||||
}
|
|
||||||
if (false) {
|
|
||||||
this.error(`Must specify --id to ${action}`, {
|
|
||||||
exit: false
|
|
||||||
});
|
|
||||||
return this._help();
|
|
||||||
}
|
|
||||||
return whereClause;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOrDeleteDevice(flags: DeviceFlags, action: Action): Promise<SprinklersDevice | never> {
|
|
||||||
const findConditions = this.getFindConditions(flags, action);
|
|
||||||
if (action === "delete") {
|
|
||||||
const result = await this.database.sprinklersDevices.delete(findConditions);
|
|
||||||
if (result.raw[1] > 0) {
|
|
||||||
this.log(`Deleted device`);
|
|
||||||
} else {
|
|
||||||
this.error("Did not find device to delete");
|
|
||||||
}
|
|
||||||
return this.exit();
|
|
||||||
} else {
|
|
||||||
const device = await this.database.sprinklersDevices.findOne(findConditions);
|
|
||||||
if (!device) {
|
|
||||||
return this.error(`The specified device does not exist`);
|
|
||||||
}
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
const parseResult = this.parse(DeviceCommand);
|
|
||||||
|
|
||||||
const flags = parseResult.flags;
|
|
||||||
const action = this.getAction(flags);
|
|
||||||
|
|
||||||
await this.connect();
|
|
||||||
|
|
||||||
if (flags.show) {
|
|
||||||
const findConditions = this.getFindConditions(flags, action);
|
|
||||||
let query = this.database.sprinklersDevices.createQueryBuilder("device")
|
|
||||||
.leftJoinAndSelect("device.users", "user")
|
|
||||||
.where(findConditions);
|
|
||||||
if (flags.username) {
|
|
||||||
query = query.where("user.username = :username", { username: flags.username });
|
|
||||||
}
|
|
||||||
const devices = await query.getMany();
|
|
||||||
if (devices.length === 0) {
|
|
||||||
this.log("No sprinklers devices found");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
this.log("Devices: ")
|
|
||||||
for (const device of devices) {
|
|
||||||
this.log(JSON.stringify(device, null, " "));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const device = await this.getOrDeleteDevice(flags, action);
|
|
||||||
|
|
||||||
if (flags["add-user"] || flags["remove-user"]) {
|
|
||||||
if (!flags.username) {
|
|
||||||
return this.error("Must specify --username for --add-user")
|
|
||||||
}
|
|
||||||
const user = await this.database.users.findByUsername(flags.username);
|
|
||||||
if (!user) {
|
|
||||||
return this.error(`Could not find user with username '${flags.username}'`);
|
|
||||||
}
|
|
||||||
const query = this.database.sprinklersDevices.createQueryBuilder()
|
|
||||||
.relation("users")
|
|
||||||
.of(device);
|
|
||||||
if (flags["add-user"]) {
|
|
||||||
await query.add(user);
|
|
||||||
this.log(`Added user '${user.username}' to device '${device.name}`);
|
|
||||||
} else {
|
|
||||||
await query.remove(user);
|
|
||||||
this.log(`Removed user '${user.username}' from device '${device.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
server/commands/manage.ts
Normal file
11
server/commands/manage.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Command from "@oclif/command";
|
||||||
|
|
||||||
|
import { createApp, ServerState, WebSocketApi } from "../";
|
||||||
|
|
||||||
|
import log from "@common/logger";
|
||||||
|
|
||||||
|
export default class ManageCommand extends Command {
|
||||||
|
run(): Promise<any> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
import { flags } from "@oclif/command";
|
|
||||||
import * as auth from "@server/authentication"
|
|
||||||
|
|
||||||
import ManageCommand from "@server/ManageCommand";
|
|
||||||
|
|
||||||
// tslint:disable:no-shadowed-variable
|
|
||||||
|
|
||||||
export default class TokenCommand extends ManageCommand {
|
|
||||||
static description = "Manage tokens";
|
|
||||||
|
|
||||||
static flags = {
|
|
||||||
"gen-device-reg": flags.boolean({
|
|
||||||
char: "d",
|
|
||||||
description: "Generate a device registration token",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
const parseResult = this.parse(TokenCommand);
|
|
||||||
|
|
||||||
const flags = parseResult.flags;
|
|
||||||
|
|
||||||
if (flags["gen-device-reg"]) {
|
|
||||||
const token = await auth.generateDeviceRegistrationToken();
|
|
||||||
this.log(`Device registration token: "${token}"`)
|
|
||||||
} else {
|
|
||||||
this.error("Must specify a command to run");
|
|
||||||
this._help();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
import { flags } from "@oclif/command";
|
|
||||||
import { ux } from "cli-ux";
|
|
||||||
import { capitalize } from "lodash";
|
|
||||||
import { FindConditions } from "typeorm";
|
|
||||||
|
|
||||||
import ManageCommand from "../ManageCommand";
|
|
||||||
|
|
||||||
import { Input } from "@oclif/parser/lib/flags";
|
|
||||||
import { User } from "@server/entities";
|
|
||||||
|
|
||||||
type UserFlags = (typeof UserCommand)["flags"] extends Input<infer F>
|
|
||||||
? F
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type Action = "show" | "create" | "update" | "delete";
|
|
||||||
|
|
||||||
// tslint:disable:no-shadowed-variable
|
|
||||||
|
|
||||||
export default class UserCommand extends ManageCommand {
|
|
||||||
static description = "Manage users";
|
|
||||||
|
|
||||||
static flags = {
|
|
||||||
show: flags.boolean({
|
|
||||||
char: "s",
|
|
||||||
exclusive: ["create", "update", "delete"],
|
|
||||||
description: "Show user(s)",
|
|
||||||
}),
|
|
||||||
create: flags.boolean({
|
|
||||||
char: "c",
|
|
||||||
exclusive: ["update", "delete", "id"],
|
|
||||||
dependsOn: ["username"],
|
|
||||||
description: "Create a new user"
|
|
||||||
}),
|
|
||||||
update: flags.boolean({
|
|
||||||
char: "u",
|
|
||||||
exclusive: ["create", "delete"],
|
|
||||||
description: "Update an existing user (by --id or --username)"
|
|
||||||
}),
|
|
||||||
delete: flags.boolean({
|
|
||||||
char: "d",
|
|
||||||
exclusive: ["create", "update"],
|
|
||||||
description: "Delete a user (by --id or --username)"
|
|
||||||
}),
|
|
||||||
id: flags.integer({
|
|
||||||
description: "The id of the user to update or delete",
|
|
||||||
}),
|
|
||||||
username: flags.string({
|
|
||||||
description: "The username of the user to create or update"
|
|
||||||
}),
|
|
||||||
name: flags.string({
|
|
||||||
description: "The name of the user, when creating or updating"
|
|
||||||
}),
|
|
||||||
passwordPrompt: flags.boolean({
|
|
||||||
char: "p",
|
|
||||||
description:
|
|
||||||
"Prompts for the password of the user when creating or updating"
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
getAction(flags: UserFlags): Action {
|
|
||||||
if (flags.show) return "show";
|
|
||||||
else if (flags.create) return "create";
|
|
||||||
else if (flags.update) return "update";
|
|
||||||
else if (flags.delete) return "delete";
|
|
||||||
else {
|
|
||||||
this.error("Must specify an action (--show, --create, --update, --delete)", {
|
|
||||||
exit: false
|
|
||||||
});
|
|
||||||
return this._help();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFindConditions(flags: UserFlags, action: Action): FindConditions<User> {
|
|
||||||
if (flags.id != null) {
|
|
||||||
return { id: flags.id };
|
|
||||||
} else if (flags.username) {
|
|
||||||
return { username: flags.username };
|
|
||||||
} else {
|
|
||||||
this.error(`Must specify either --id or --username to ${action}`, {
|
|
||||||
exit: false
|
|
||||||
});
|
|
||||||
return this._help();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOrDeleteUser(flags: UserFlags, action: Action): Promise<User | never> {
|
|
||||||
if (action === "create") {
|
|
||||||
return this.database.users.create();
|
|
||||||
} else {
|
|
||||||
const findConditions = this.getFindConditions(flags, action);
|
|
||||||
if (action === "delete") {
|
|
||||||
const result = await this.database.users.delete(findConditions);
|
|
||||||
this.log(`Deleted user: `, result);
|
|
||||||
return this.exit();
|
|
||||||
} else {
|
|
||||||
const user = await this.database.users.findOneUser(findConditions);
|
|
||||||
if (!user) {
|
|
||||||
return this.error(`The specified user does not exist`);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
const parseResult = this.parse(UserCommand);
|
|
||||||
|
|
||||||
const flags = parseResult.flags;
|
|
||||||
const action = this.getAction(flags);
|
|
||||||
|
|
||||||
await this.connect();
|
|
||||||
|
|
||||||
if (flags.show) {
|
|
||||||
let whereClause: FindConditions<User> = {};
|
|
||||||
if (flags.id) {
|
|
||||||
whereClause.id = flags.id;
|
|
||||||
}
|
|
||||||
if (flags.username) {
|
|
||||||
whereClause.username = flags.username;
|
|
||||||
}
|
|
||||||
const users = await this.database.users.find({ where: whereClause });
|
|
||||||
if (users.length == 0) {
|
|
||||||
this.error("No users found");
|
|
||||||
}
|
|
||||||
this.log("Users: ")
|
|
||||||
for (const user of users) {
|
|
||||||
this.log(JSON.stringify(user.toJSON()));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.getOrDeleteUser(flags, action);
|
|
||||||
|
|
||||||
if (flags.username && (flags.create || flags.id)) {
|
|
||||||
user.username = flags.username;
|
|
||||||
}
|
|
||||||
if (flags.name) {
|
|
||||||
user.name = flags.name;
|
|
||||||
}
|
|
||||||
if (flags.passwordPrompt || flags.create) {
|
|
||||||
const password = await ux.prompt("Enter a password to assign the user", {
|
|
||||||
type: "hide"
|
|
||||||
});
|
|
||||||
await user.setPassword(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.database.users.save(user);
|
|
||||||
this.log(`${capitalize(action)}d user id ${user.id} (${user.username})`);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,7 +14,7 @@ export class SprinklersDevice implements ISprinklersDevice {
|
|||||||
@Column()
|
@Column()
|
||||||
name: string = "";
|
name: string = "";
|
||||||
|
|
||||||
@ManyToMany(type => User, user => user.devices)
|
@ManyToMany(type => User)
|
||||||
users: User[] | undefined;
|
users: User[] | undefined;
|
||||||
|
|
||||||
constructor(data?: Partial<SprinklersDevice>) {
|
constructor(data?: Partial<SprinklersDevice>) {
|
||||||
|
@ -3,7 +3,6 @@ import { omit } from "lodash";
|
|||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
Index,
|
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
PrimaryGeneratedColumn
|
PrimaryGeneratedColumn
|
||||||
@ -19,8 +18,7 @@ export class User implements IUser {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column()
|
@Column({ unique: true })
|
||||||
@Index("user_username_unique", { unique: true })
|
|
||||||
username: string = "";
|
username: string = "";
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
@ -29,7 +27,7 @@ export class User implements IUser {
|
|||||||
@Column()
|
@Column()
|
||||||
passwordHash: string = "";
|
passwordHash: string = "";
|
||||||
|
|
||||||
@ManyToMany(type => SprinklersDevice, device => device.users)
|
@ManyToMany(type => SprinklersDevice)
|
||||||
@JoinTable({ name: "user_sprinklers_device" })
|
@JoinTable({ name: "user_sprinklers_device" })
|
||||||
devices: SprinklersDevice[] | undefined;
|
devices: SprinklersDevice[] | undefined;
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { Request } from "express";
|
|
||||||
import PromiseRouter from "express-promise-router";
|
import PromiseRouter from "express-promise-router";
|
||||||
import { serialize } from "serializr";
|
import { serialize } from "serializr";
|
||||||
|
|
||||||
import ApiError from "@common/ApiError";
|
import ApiError from "@common/ApiError";
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import * as schema from "@common/sprinklersRpc/schema";
|
import * as schema from "@common/sprinklersRpc/schema";
|
||||||
import { DeviceToken } from "@common/TokenClaims";
|
|
||||||
import { generateDeviceToken } from "@server/authentication";
|
import { generateDeviceToken } from "@server/authentication";
|
||||||
import { SprinklersDevice } from "@server/entities";
|
|
||||||
import { verifyAuthorization } from "@server/express/verifyAuthorization";
|
import { verifyAuthorization } from "@server/express/verifyAuthorization";
|
||||||
import { ServerState } from "@server/state";
|
import { ServerState } from "@server/state";
|
||||||
|
|
||||||
@ -33,7 +30,7 @@ function randomDeviceId(): string {
|
|||||||
export function devices(state: ServerState) {
|
export function devices(state: ServerState) {
|
||||||
const router = PromiseRouter();
|
const router = PromiseRouter();
|
||||||
|
|
||||||
async function verifyUserDevice(req: Request): Promise<SprinklersDevice> {
|
router.get("/:deviceId", verifyAuthorization(), async (req, res) => {
|
||||||
const token = req.token!;
|
const token = req.token!;
|
||||||
const userId = token.aud;
|
const userId = token.aud;
|
||||||
const deviceId = req.params.deviceId;
|
const deviceId = req.params.deviceId;
|
||||||
@ -47,39 +44,12 @@ export function devices(state: ServerState) {
|
|||||||
ErrorCode.NoPermission
|
ErrorCode.NoPermission
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return userDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get("/:deviceId", verifyAuthorization(), async (req, res) => {
|
|
||||||
const deviceInfo = await verifyUserDevice(req);
|
|
||||||
res.send({
|
|
||||||
id: deviceInfo.id, deviceId: deviceInfo.deviceId, name: deviceInfo.name
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/:deviceId/data", verifyAuthorization(), async (req, res) => {
|
|
||||||
await verifyUserDevice(req);
|
|
||||||
const device = state.mqttClient.acquireDevice(req.params.deviceId);
|
const device = state.mqttClient.acquireDevice(req.params.deviceId);
|
||||||
const j = serialize(schema.sprinklersDevice, device);
|
const j = serialize(schema.sprinklersDevice, device);
|
||||||
res.send(j);
|
res.send(j);
|
||||||
device.release();
|
device.release();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/:deviceId/generate_token",
|
|
||||||
verifyAuthorization(), async (req, res) => {
|
|
||||||
const device = await verifyUserDevice(req);
|
|
||||||
if (!device.deviceId) {
|
|
||||||
throw new ApiError(
|
|
||||||
"A token cannot be granted for a device with no id",
|
|
||||||
ErrorCode.BadRequest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const token = await generateDeviceToken(device.id, device.deviceId);
|
|
||||||
res.send({
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/register",
|
"/register",
|
||||||
verifyAuthorization({
|
verifyAuthorization({
|
||||||
@ -106,19 +76,8 @@ export function devices(state: ServerState) {
|
|||||||
type: "device"
|
type: "device"
|
||||||
}),
|
}),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const token: DeviceToken = req.token! as any;
|
|
||||||
const deviceId = token.aud;
|
|
||||||
const devs = await state.database.sprinklersDevices.count({
|
|
||||||
deviceId
|
|
||||||
});
|
|
||||||
if (devs === 0) {
|
|
||||||
throw new ApiError("deviceId not found", ErrorCode.NotFound);
|
|
||||||
}
|
|
||||||
const clientId = `device-${deviceId}`;
|
|
||||||
res.send({
|
res.send({
|
||||||
mqttUrl: state.mqttUrl,
|
url: state.mqttUrl
|
||||||
deviceId,
|
|
||||||
clientId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
import pinoHttp = require("pino-http");
|
import expressPinoLogger = require("express-pino-logger");
|
||||||
|
import * as pino from "pino";
|
||||||
|
|
||||||
export default pinoHttp({
|
const l = pino();
|
||||||
logger: log,
|
pino(l);
|
||||||
useLevel: "debug",
|
|
||||||
} as pinoHttp.Options);
|
export default expressPinoLogger(log);
|
||||||
|
@ -5,6 +5,5 @@ import "./env";
|
|||||||
import "./configureLogger";
|
import "./configureLogger";
|
||||||
|
|
||||||
export { ServerState } from "./state";
|
export { ServerState } from "./state";
|
||||||
export { Database } from "./Database";
|
|
||||||
export { createApp } from "./express";
|
export { createApp } from "./express";
|
||||||
export { WebSocketApi } from "./sprinklersRpc/";
|
export { WebSocketApi } from "./sprinklersRpc/";
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { DeepPartial, EntityRepository, Repository, SaveOptions } from "typeorm";
|
import { EntityRepository, Repository } from "typeorm";
|
||||||
|
|
||||||
import { SprinklersDevice, User } from "@server/entities";
|
import { SprinklersDevice, User } from "@server/entities";
|
||||||
import UniqueConstraintError from "@server/UniqueConstraintError";
|
|
||||||
|
|
||||||
@EntityRepository(SprinklersDevice)
|
@EntityRepository(SprinklersDevice)
|
||||||
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
||||||
@ -40,17 +39,4 @@ export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
|||||||
}
|
}
|
||||||
return user.devices![0];
|
return user.devices![0];
|
||||||
}
|
}
|
||||||
|
|
||||||
save<T extends DeepPartial<SprinklersDevice>>(entities: T[], options?: SaveOptions): Promise<T[]>;
|
|
||||||
save<T extends DeepPartial<SprinklersDevice>>(entity: T, options?: SaveOptions): Promise<T>;
|
|
||||||
async save(entity: any, options?: SaveOptions): Promise<any> {
|
|
||||||
try {
|
|
||||||
return await super.save(entity, options);
|
|
||||||
} catch (e) {
|
|
||||||
if (UniqueConstraintError.is(e)) {
|
|
||||||
throw new UniqueConstraintError(e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,49 +1,33 @@
|
|||||||
import { EntityRepository, FindConditions, FindOneOptions, Repository } from "typeorm";
|
import { EntityRepository, FindOneOptions, Repository } from "typeorm";
|
||||||
|
|
||||||
import { User } from "@server/entities";
|
import { User } from "@server/entities";
|
||||||
|
|
||||||
export interface FindUserOptions extends FindOneOptions<User> {
|
export interface FindUserOptions {
|
||||||
devices: boolean;
|
devices: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeOptions(
|
function applyDefaultOptions(
|
||||||
options?: Partial<FindUserOptions>
|
options?: Partial<FindUserOptions>
|
||||||
): FindOneOptions<User> {
|
): FindOneOptions<User> {
|
||||||
const opts: FindUserOptions = { devices: false, ...options };
|
const opts: FindUserOptions = { devices: false, ...options };
|
||||||
const { devices, ...rest } = opts;
|
const relations = [opts.devices && "devices"].filter(Boolean) as string[];
|
||||||
const relations = [devices && "devices"].filter(Boolean) as string[];
|
return { relations };
|
||||||
return { relations, ...rest };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@EntityRepository(User)
|
@EntityRepository(User)
|
||||||
export class UserRepository extends Repository<User> {
|
export class UserRepository extends Repository<User> {
|
||||||
findAll(options?: Partial<FindUserOptions>) {
|
findAll(options?: Partial<FindUserOptions>) {
|
||||||
const opts = computeOptions(options);
|
const opts = applyDefaultOptions(options);
|
||||||
return super.find(opts);
|
return super.find(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
findOneUser(conditions: FindConditions<User>, options?: Partial<FindUserOptions>) {
|
|
||||||
const opts = computeOptions(options);
|
|
||||||
return super.findOne(conditions, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
findById(id: number, options?: Partial<FindUserOptions>) {
|
findById(id: number, options?: Partial<FindUserOptions>) {
|
||||||
return this.findOneUser({ id }, options);
|
const opts = applyDefaultOptions(options);
|
||||||
|
return super.findOne(id, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
findByUsername(username: string, options?: Partial<FindUserOptions>) {
|
findByUsername(username: string, options?: Partial<FindUserOptions>) {
|
||||||
return this.findOneUser({ username }, options);
|
const opts = applyDefaultOptions(options);
|
||||||
|
return this.findOne({ username }, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// async checkAndSave(entity: User): Promise<User> {
|
|
||||||
// return this.manager.transaction(manager => {
|
|
||||||
// let query = manager.createQueryBuilder<User>(User, "user", manager.queryRunner!);
|
|
||||||
// if (entity.id != null) {
|
|
||||||
// query = query.where("user.id <> :id", { id: entity.id })
|
|
||||||
// }
|
|
||||||
// query
|
|
||||||
|
|
||||||
// return manager.save(entity);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -23,20 +23,13 @@ export class ServerState {
|
|||||||
|
|
||||||
async startDatabase() {
|
async startDatabase() {
|
||||||
await this.database.connect();
|
await this.database.connect();
|
||||||
|
logger.info("connected to database");
|
||||||
|
|
||||||
if (process.env.INSERT_TEST_DATA) {
|
if (process.env.INSERT_TEST_DATA) {
|
||||||
try {
|
await this.database.insertTestData();
|
||||||
await this.database.insertTestData();
|
logger.info("inserted test data");
|
||||||
logger.info("inserted test data");
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e, "error inserting test data");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopDatabase() {
|
|
||||||
await this.database.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
async startMqtt() {
|
async startMqtt() {
|
||||||
this.mqttClient.username = SUPERUSER;
|
this.mqttClient.username = SUPERUSER;
|
||||||
|
@ -2,25 +2,24 @@
|
|||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../dist",
|
"outDir": "../dist",
|
||||||
"sourceMap": true,
|
// "sourceMap": true,
|
||||||
"emitDecoratorMetadata": true,
|
// "emitDecoratorMetadata": true,
|
||||||
"strict": true,
|
// "strict": true,
|
||||||
"allowJs": true,
|
// "allowJs": true,
|
||||||
"baseUrl": "..",
|
// "baseUrl": "..",
|
||||||
"paths": {
|
// "paths": {
|
||||||
"@common/*": [
|
// "@common/*": [
|
||||||
"./common/*"
|
// "./common/*"
|
||||||
],
|
// ],
|
||||||
"@server/*": [
|
// "@server/*": [
|
||||||
"./server/*"
|
// "./server/*"
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
},
|
}//,
|
||||||
"references": [{
|
// "references": [{
|
||||||
"path": "../common"
|
// "path": "../common"
|
||||||
}],
|
// }],
|
||||||
"include": [
|
// "include": [
|
||||||
"./**/*.ts"
|
// "./**/*.ts"
|
||||||
],
|
// ]
|
||||||
"exclude": []
|
|
||||||
}
|
}
|
7
server/types/express-pino-logger.d.ts
vendored
Normal file
7
server/types/express-pino-logger.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
declare module "express-pino-logger" {
|
||||||
|
import { Logger } from "pino";
|
||||||
|
import { ErrorRequestHandler } from "express";
|
||||||
|
|
||||||
|
function makeLogger(logger: Logger): ErrorRequestHandler;
|
||||||
|
export = makeLogger;
|
||||||
|
}
|
@ -9,20 +9,6 @@
|
|||||||
"types": [
|
"types": [
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
"strict": true,
|
"strict": true
|
||||||
"baseUrl": ".",
|
}
|
||||||
"paths": {
|
|
||||||
"@common/*": [
|
|
||||||
"./common/*"
|
|
||||||
],
|
|
||||||
"@client/*": [
|
|
||||||
"./client/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"./client",
|
|
||||||
"./common",
|
|
||||||
"./server"
|
|
||||||
]
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user