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/
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY tsconfig.json tslint.json paths.js /app/
|
||||
COPY bin/ /app/bin
|
||||
COPY tslint.json /app
|
||||
COPY paths.js /app
|
||||
COPY client/ /app/client
|
||||
COPY common/ /app/common
|
||||
COPY server/ /app/server
|
||||
@ -19,11 +19,10 @@ FROM node:10
|
||||
|
||||
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/bin ./bin
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT [ "node", ".", "serve"]
|
||||
ENTRYPOINT [ "node", "." ]
|
||||
|
@ -5,7 +5,8 @@ WORKDIR /app/
|
||||
COPY package.json yarn.lock /app/
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY tsconfig.json tslint.json paths.js /app/
|
||||
COPY paths.js /app
|
||||
COPY tslint.json /app
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT [ "npm", "run", "start:dev" ]
|
||||
|
@ -1,6 +1,6 @@
|
||||
// import DevTools from "mobx-react-devtools";
|
||||
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 { MessagesView, NavBar } from "@client/components";
|
||||
@ -30,7 +30,7 @@ function NavContainer() {
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
export default function App() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={route.login} component={p.LoginPage} />
|
||||
@ -39,5 +39,3 @@ function App() {
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(App);
|
||||
|
@ -2,7 +2,7 @@ import * as classNames from "classnames";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
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 * as p from "@client/pages";
|
||||
@ -62,7 +62,7 @@ const ConnectionState = observer(
|
||||
}
|
||||
);
|
||||
|
||||
interface DeviceViewProps extends RouteComponentProps {
|
||||
interface DeviceViewProps {
|
||||
deviceId: number;
|
||||
appState: AppState;
|
||||
inList?: boolean;
|
||||
@ -87,13 +87,11 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
||||
return null;
|
||||
}
|
||||
const { connectionState, sectionRunner, sections } = this.device;
|
||||
const dimmed = !connectionState.isDeviceConnected;
|
||||
if (!connectionState.isAvailable || inList) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dimmer.Dimmable blurring dimmed={dimmed} className="device-body">
|
||||
<Dimmer active={dimmed} />
|
||||
<React.Fragment>
|
||||
<Grid>
|
||||
<Grid.Column mobile="16" tablet="16" computer="16" largeScreen="6">
|
||||
<SectionRunnerView
|
||||
@ -117,7 +115,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
||||
path={route.program(":deviceId", ":programId")}
|
||||
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";
|
||||
|
||||
export interface DurationViewProps {
|
||||
export default class DurationView extends React.Component<{
|
||||
label?: string;
|
||||
inline?: boolean;
|
||||
duration: Duration;
|
||||
onDurationChange?: (newDuration: Duration) => void;
|
||||
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() {
|
||||
const { duration, label, inline, onDurationChange, className } = this.props;
|
||||
const inputsClassName = classNames("durationInputs", { inline });
|
||||
@ -99,18 +22,24 @@ export default class DurationView extends React.Component<DurationViewProps> {
|
||||
<Form.Field inline={inline} className={className}>
|
||||
{label && <label>{label}</label>}
|
||||
<div className={inputsClassName}>
|
||||
<NumberInput
|
||||
<Input
|
||||
type="number"
|
||||
className="durationInput minutes"
|
||||
value={this.props.duration.minutes}
|
||||
value={duration.minutes}
|
||||
onChange={this.onMinutesChange}
|
||||
label="M"
|
||||
labelPosition="right"
|
||||
onWheel={this.onWheel}
|
||||
/>
|
||||
<NumberInput
|
||||
<Input
|
||||
type="number"
|
||||
className="durationInput seconds"
|
||||
value={this.props.duration.seconds}
|
||||
value={duration.seconds}
|
||||
onChange={this.onSecondsChange}
|
||||
max={60}
|
||||
max="60"
|
||||
label="S"
|
||||
labelPosition="right"
|
||||
onWheel={this.onWheel}
|
||||
/>
|
||||
</div>
|
||||
</Form.Field>
|
||||
@ -126,25 +55,23 @@ export default class DurationView extends React.Component<DurationViewProps> {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Readonly<DurationViewProps>) {
|
||||
if (nextProps.duration.minutes !== this.props.duration.minutes ||
|
||||
nextProps.duration.seconds !== this.props.duration.seconds) {
|
||||
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));
|
||||
private onMinutesChange: InputProps["onChange"] = (e, { value }) => {
|
||||
if (!this.props.onDurationChange || isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
const newMinutes = Number(value);
|
||||
this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
|
||||
};
|
||||
|
||||
private onSecondsChange = (newSeconds: number) => {
|
||||
if (this.props.onDurationChange) {
|
||||
this.props.onDurationChange(this.props.duration.withSeconds(newSeconds));
|
||||
private onSecondsChange: InputProps["onChange"] = (e, { value }) => {
|
||||
if (!this.props.onDurationChange || isNaN(Number(value))) {
|
||||
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) {
|
||||
message.onDismiss(event, data);
|
||||
}
|
||||
uiStore.removeMessage(message);
|
||||
uiStore.messages.remove(message);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -105,17 +105,16 @@ class ProgramSequenceItem extends React.Component<{
|
||||
|
||||
const ProgramSequenceItemD = SortableElement(ProgramSequenceItem);
|
||||
|
||||
// tslint:disable: no-shadowed-variable
|
||||
const ProgramSequenceList = SortableContainer(
|
||||
observer(
|
||||
function ProgramSequenceList(props: {
|
||||
(props: {
|
||||
className: string;
|
||||
list: ProgramItem[];
|
||||
sections: Section[];
|
||||
editing: boolean;
|
||||
onChange: ItemChangeHandler;
|
||||
onRemove: ItemRemoveHandler;
|
||||
}) {
|
||||
}) => {
|
||||
const { className, list, sections, ...rest } = props;
|
||||
const listItems = list.map((item, index) => {
|
||||
const key = `item-${index}`;
|
||||
@ -133,6 +132,7 @@ const ProgramSequenceList = SortableContainer(
|
||||
return <ul className={className}>{listItems}</ul>;
|
||||
}
|
||||
),
|
||||
{ withRef: true }
|
||||
);
|
||||
|
||||
@observer
|
||||
|
@ -8,7 +8,6 @@ import { ProgramSequenceView, ScheduleView } from "@client/components";
|
||||
import * as route from "@client/routePaths";
|
||||
import { ISprinklersDevice } from "@common/httpApi";
|
||||
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
||||
import moment = require("moment");
|
||||
|
||||
@observer
|
||||
class ProgramRows extends React.Component<{
|
||||
@ -70,12 +69,6 @@ class ProgramRows extends React.Component<{
|
||||
<h4>Sequence: </h4>{" "}
|
||||
<ProgramSequenceView sequence={sequence} sections={sections} />
|
||||
<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>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-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 { UiStore } from "@client/state";
|
||||
@ -30,14 +30,8 @@ export default class RunSectionForm extends React.Component<
|
||||
|
||||
render() {
|
||||
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 (
|
||||
<Segment className="runSectionForm">
|
||||
<Segment>
|
||||
<Header>Run Section</Header>
|
||||
<Form>
|
||||
<SectionChooser
|
||||
@ -51,14 +45,10 @@ export default class RunSectionForm extends React.Component<
|
||||
duration={duration}
|
||||
onDurationChange={this.onDurationChange}
|
||||
/>
|
||||
{
|
||||
this.isValid ? runButton :
|
||||
<Popup trigger={runButton} on={["click", "hover"]} position="right center">
|
||||
<Popup.Content>
|
||||
Select a section to run and a duration
|
||||
</Popup.Content>
|
||||
</Popup>
|
||||
}
|
||||
<Form.Button primary onClick={this.run} disabled={!this.isValid}>
|
||||
<Icon name="play" />
|
||||
Run
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Segment>
|
||||
);
|
||||
|
@ -1,18 +1,14 @@
|
||||
import { observer } from "mobx-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 { AppState, injectState } from "@client/state";
|
||||
|
||||
class DevicesPage extends React.Component<{ appState: AppState }> {
|
||||
refreshDevices = () => {
|
||||
this.props.appState.sprinklersRpc.doAuthenticate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { appState } = this.props;
|
||||
const userData = appState.userStore.getUserData();
|
||||
const { userData } = appState.userStore;
|
||||
let deviceNodes: React.ReactNode;
|
||||
if (!userData) {
|
||||
deviceNodes = <span>Not logged in</span>;
|
||||
@ -25,7 +21,7 @@ class DevicesPage extends React.Component<{ appState: AppState }> {
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1 className="devices-header">Devices <Button icon="refresh" onClick={this.refreshDevices} /></h1>
|
||||
<h1>Devices</h1>
|
||||
<Item.Group>{deviceNodes}</Item.Group>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import { assign } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import * as qs from "query-string";
|
||||
import * as React from "react";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import {
|
||||
Button,
|
||||
CheckboxProps,
|
||||
@ -20,9 +20,7 @@ import { AppState, injectState } from "@client/state";
|
||||
import { ISprinklersDevice } from "@common/httpApi";
|
||||
import log from "@common/logger";
|
||||
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
||||
import classNames = require("classnames");
|
||||
import { action } from "mobx";
|
||||
import * as moment from "moment";
|
||||
|
||||
interface ProgramPageProps
|
||||
extends RouteComponentProps<{ deviceId: string; programId: string }> {
|
||||
@ -175,10 +173,8 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
||||
|
||||
const { running, enabled, schedule, sequence } = program;
|
||||
|
||||
const className = classNames("programEditor", editing && "editing");
|
||||
|
||||
return (
|
||||
<Modal open onClose={this.close} className={className}>
|
||||
<Modal open onClose={this.close} className="programEditor">
|
||||
<Modal.Header>{this.renderName(program)}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Form>
|
||||
@ -187,6 +183,7 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
||||
toggle
|
||||
label="Enabled"
|
||||
checked={enabled}
|
||||
readOnly={!editing}
|
||||
onChange={this.onEnabledChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
@ -211,16 +208,6 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
||||
editing={editing}
|
||||
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>
|
||||
</Modal.Content>
|
||||
{this.renderActions(program)}
|
||||
@ -253,10 +240,6 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
||||
},
|
||||
err => {
|
||||
log.error({ err }, "error updating Program");
|
||||
this.props.appState.uiStore.addMessage({
|
||||
error: true,
|
||||
content: `Error updating program: ${err}`,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.stopEditing();
|
||||
@ -282,28 +265,11 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
||||
|
||||
@action.bound
|
||||
private onEnabledChange(e: any, p: CheckboxProps) {
|
||||
if (p.checked !== undefined && this.program) {
|
||||
this.program.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}`,
|
||||
});
|
||||
}
|
||||
);
|
||||
if (this.programView) {
|
||||
this.programView.enabled = p.checked!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DecoratedProgramPage = injectState(withRouter(ProgramPage));
|
||||
const DecoratedProgramPage = injectState(observer(ProgramPage));
|
||||
export default DecoratedProgramPage;
|
||||
|
@ -92,10 +92,6 @@ export class WebSocketRpcClient extends s.SprinklersRPC {
|
||||
this._connect();
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
this._connect();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.reconnectTimer != null) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
@ -136,29 +132,27 @@ export class WebSocketRpcClient extends s.SprinklersRPC {
|
||||
() =>
|
||||
this.connectionState.clientToServer === true &&
|
||||
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
|
||||
async makeDeviceCall(
|
||||
deviceId: string,
|
||||
@ -195,16 +189,11 @@ export class WebSocketRpcClient extends s.SprinklersRPC {
|
||||
const id = this.nextRequestId++;
|
||||
return new Promise<ws.IServerResponseTypes[Method]>((resolve, reject) => {
|
||||
let timeoutHandle: number;
|
||||
this.responseCallbacks[id] = (response: ws.ServerResponse) => {
|
||||
this.responseCallbacks[id] = response => {
|
||||
clearTimeout(timeoutHandle);
|
||||
delete this.responseCallbacks[id];
|
||||
if (response.result === "success") {
|
||||
if (response.method === method) {
|
||||
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 }));
|
||||
}
|
||||
resolve(response.data);
|
||||
} else {
|
||||
const { error } = response;
|
||||
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);
|
||||
this.sprinklersRpc.start();
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", this.onPageFocus);
|
||||
}
|
||||
|
||||
onPageFocus = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this.sprinklersRpc.reconnect();
|
||||
}
|
||||
};
|
||||
|
||||
@computed
|
||||
get isLoggedIn() {
|
||||
return this.tokenStore.accessToken.isValid;
|
||||
@ -55,7 +47,7 @@ export default class AppState extends TypedEventEmitter<AppEvents> {
|
||||
|
||||
async start() {
|
||||
configure({
|
||||
enforceActions: "observed"
|
||||
enforceActions: true
|
||||
});
|
||||
|
||||
syncHistoryWithStore(this.history, this.routerStore);
|
||||
|
@ -1,24 +1,20 @@
|
||||
import { ISprinklersDevice, IUser } from "@common/httpApi";
|
||||
import { action, IObservableValue, observable } from "mobx";
|
||||
import { action, observable } from "mobx";
|
||||
|
||||
export class UserStore {
|
||||
userData: IObservableValue<IUser | null> = observable.box(null);
|
||||
@observable
|
||||
userData: IUser | null = null;
|
||||
|
||||
@action.bound
|
||||
receiveUserData(userData: IUser) {
|
||||
this.userData.set(userData);
|
||||
}
|
||||
|
||||
getUserData(): IUser | null {
|
||||
return this.userData.get();
|
||||
this.userData = userData;
|
||||
}
|
||||
|
||||
findDevice(id: number): ISprinklersDevice | null {
|
||||
const userData = this.userData.get();
|
||||
return (
|
||||
(userData &&
|
||||
userData.devices &&
|
||||
userData.devices.find(dev => dev.id === id)) ||
|
||||
(this.userData &&
|
||||
this.userData.devices &&
|
||||
this.userData.devices.find(dev => dev.id === id)) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
@ -31,18 +31,26 @@ export function ConsumeState({ children }: ConsumeStateProps) {
|
||||
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 }>(
|
||||
Component: React.ComponentType<P>
|
||||
): React.FunctionComponent<Omit<P, "appState">> {
|
||||
return function InjectState(props) {
|
||||
const state = React.useContext(StateContext);
|
||||
if (state == null) {
|
||||
throw new Error(
|
||||
"Component with injectState must be mounted inside ProvideState"
|
||||
);
|
||||
): React.ComponentClass<Omit<P, "appState">> {
|
||||
return class extends React.Component<Omit<P, "appState">> {
|
||||
render() {
|
||||
const consumeState = (state: AppState | null) => {
|
||||
if (state == null) {
|
||||
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 {
|
||||
.header {
|
||||
display: flex !important;
|
||||
@ -16,29 +6,6 @@ $connected-color: #13d213;
|
||||
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 {
|
||||
margin-top: 0;
|
||||
}
|
||||
@ -50,11 +17,11 @@ $connected-color: #13d213;
|
||||
font-weight: lighter;
|
||||
|
||||
&.connected {
|
||||
color: $connected-color;
|
||||
color: #13d213;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
color: $disconnected-color;
|
||||
color: #d20000;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,24 +45,6 @@ $connected-color: #13d213;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.program--nextRun {
|
||||
display: inline-block;
|
||||
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;
|
||||
}
|
||||
.ui.modal.programEditor > .header > .header.item .inline.fields {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ $durationInput-labelWidth: 2.5em;
|
||||
|
||||
.field .durationInputs {
|
||||
display: flex; // max-width: 100%;
|
||||
justify-content: flex-start;
|
||||
justify-content: start;
|
||||
flex-wrap: wrap;
|
||||
margin: -$durationInput-spacing / 2;
|
||||
|
||||
|
@ -8,16 +8,12 @@
|
||||
.programSequence-item {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5em;
|
||||
&.dragging {
|
||||
z-index: 1010;
|
||||
}
|
||||
.fields {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0em !important;
|
||||
padding: 0em !important;
|
||||
margin: 0em 0em 1em !important;
|
||||
}
|
||||
.ui.icon.button {
|
||||
height: fit-content;
|
||||
|
@ -10,7 +10,7 @@
|
||||
],
|
||||
"types": [
|
||||
"webpack-env",
|
||||
// "core-js",
|
||||
"core-js",
|
||||
"node"
|
||||
],
|
||||
"baseUrl": "..",
|
||||
@ -24,10 +24,9 @@
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
"./client/**/*.ts",
|
||||
"./client/**/*.tsx"
|
||||
],
|
||||
"exclude": [],
|
||||
"references": [{
|
||||
"path": "../common"
|
||||
}]
|
||||
|
@ -2,9 +2,9 @@ const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
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 TerserPlugin = require("terser-webpack-plugin");
|
||||
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
|
||||
const DashboardPlugin = require("webpack-dashboard/plugin");
|
||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||
.BundleAnalyzerPlugin;
|
||||
@ -110,7 +110,6 @@ function getConfig(env) {
|
||||
},
|
||||
cssRule,
|
||||
sassRule,
|
||||
// Process TypeScript with TSC through HappyPack.
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
include: [paths.clientDir, paths.commonDir],
|
||||
@ -126,7 +125,7 @@ function getConfig(env) {
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
configFile: paths.clientTsConfig,
|
||||
happyPackMode: true
|
||||
transpileOnly: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -170,17 +169,21 @@ function getConfig(env) {
|
||||
}
|
||||
: undefined
|
||||
}),
|
||||
// new FaviconsWebpackPlugin({
|
||||
// logo: path.resolve(paths.clientDir, "images", "favicon-96x96.png"),
|
||||
// emitStatis: false,
|
||||
// prefix: "static/icons-[hash]/"
|
||||
// }),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: path.resolve(paths.clientDir, "images", "favicon-96x96.png"),
|
||||
emitStatis: false,
|
||||
prefix: "static/icons-[hash]/"
|
||||
}),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === "production") { ... }. See `./env.js`.
|
||||
// It is absolutely essential that NODE_ENV was set to production here.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(environ.stringified),
|
||||
new CaseSensitivePathsPlugin(),
|
||||
isProd &&
|
||||
new UglifyJsPlugin({
|
||||
sourceMap: shouldUseSourceMap
|
||||
}),
|
||||
isDev && new webpack.HotModuleReplacementPlugin(),
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
checkSyntacticErrors: true,
|
||||
@ -235,17 +238,13 @@ function getConfig(env) {
|
||||
extensions: [".ts", ".tsx", ".js", ".json", ".scss"],
|
||||
alias: {
|
||||
"@client": paths.clientDir,
|
||||
"@common": paths.commonDir,
|
||||
"react-dom": isDev ? "@hot-loader/react-dom" : "react-dom"
|
||||
"@common": paths.commonDir
|
||||
}
|
||||
},
|
||||
module: { rules },
|
||||
plugins: plugins,
|
||||
optimization: {
|
||||
namedModules: isProd,
|
||||
minimizer: isProd ? [new TerserPlugin({
|
||||
sourceMap: shouldUseSourceMap
|
||||
})] : [],
|
||||
namedModules: isProd
|
||||
},
|
||||
devServer: {
|
||||
hot: true,
|
||||
@ -258,7 +257,7 @@ function getConfig(env) {
|
||||
target: paths.publicUrl
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,9 @@ export enum ErrorCode {
|
||||
BadToken = 105,
|
||||
Unauthorized = 106,
|
||||
NoPermission = 107,
|
||||
NotImplemented = 108,
|
||||
NotFound = 109,
|
||||
NotUnique = 110,
|
||||
Internal = 200,
|
||||
NotImplemented = 201,
|
||||
Timeout = 300,
|
||||
ServerDisconnected = 301,
|
||||
BrokerDisconnected = 302
|
||||
@ -23,7 +22,6 @@ export function toHttpStatus(errorCode: ErrorCode): number {
|
||||
case ErrorCode.Parse:
|
||||
case ErrorCode.Range:
|
||||
case ErrorCode.InvalidData:
|
||||
case ErrorCode.NotUnique:
|
||||
return 400; // Bad request
|
||||
case ErrorCode.Unauthorized:
|
||||
case ErrorCode.BadToken:
|
||||
|
@ -133,7 +133,7 @@ export type IResponseHandler<
|
||||
ResponseTypes,
|
||||
ErrorType,
|
||||
Method extends keyof ResponseTypes = keyof ResponseTypes
|
||||
> = (response: Response<ResponseTypes, ErrorType, Method>) => void;
|
||||
> = (response: ResponseData<ResponseTypes, ErrorType, Method>) => void;
|
||||
|
||||
export interface ResponseHandlers<
|
||||
ResponseTypes = DefaultResponseTypes,
|
||||
|
@ -32,8 +32,6 @@ export class Program {
|
||||
sequence: ProgramItem[] = [];
|
||||
@observable
|
||||
running: boolean = false;
|
||||
@observable
|
||||
nextRun: Date | null = null;
|
||||
|
||||
constructor(device: SprinklersDevice, id: number, data?: Partial<Program>) {
|
||||
this.device = device;
|
||||
@ -62,8 +60,7 @@ export class Program {
|
||||
enabled: this.enabled,
|
||||
running: this.running,
|
||||
schedule: this.schedule.clone(),
|
||||
sequence: this.sequence.slice(),
|
||||
nextRun: this.nextRun,
|
||||
sequence: this.sequence.slice()
|
||||
});
|
||||
}
|
||||
|
||||
@ -71,7 +68,7 @@ export class Program {
|
||||
return (
|
||||
`Program{name="${this.name}", enabled=${this.enabled}, 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) {
|
||||
if (topic === "running") {
|
||||
this.running = payload === "true";
|
||||
} else if (topic === "nextRun") {
|
||||
this.nextRun = (payload.length > 0) ? new Date(Number(payload) * 1000.0) : null;
|
||||
} else if (topic == null) {
|
||||
this.updateFromJSON(JSON.parse(payload));
|
||||
}
|
||||
|
@ -218,7 +218,7 @@ class MqttSprinklersDevice extends s.SprinklersDevice {
|
||||
}
|
||||
|
||||
doUnsubscribe() {
|
||||
this.apiClient.client.unsubscribe(this.subscriptions, (err: Error | undefined) => {
|
||||
this.apiClient.client.unsubscribe(this.subscriptions, err => {
|
||||
if (err) {
|
||||
log.error({ err, id: this.id }, "error unsubscribing to device");
|
||||
} else {
|
||||
|
@ -1,29 +1,28 @@
|
||||
import {Context, custom, ModelSchema, primitive, PropSchema} from 'serializr';
|
||||
|
||||
import * as s from '..';
|
||||
import { ModelSchema, primitive, PropSchema } from "serializr";
|
||||
import * as s from "..";
|
||||
|
||||
export const duration: PropSchema = primitive();
|
||||
|
||||
export const date: PropSchema = custom(
|
||||
(jsDate: Date|null) => jsDate != null ? jsDate.toISOString() : null,
|
||||
(json: any, context: Context, oldValue: any,
|
||||
done: (err: any, value: any) => void) => {
|
||||
if (json === null) {
|
||||
return done(null, null);
|
||||
}
|
||||
try {
|
||||
done(null, new Date(json));
|
||||
} catch (e) {
|
||||
done(e, undefined);
|
||||
}
|
||||
});
|
||||
export const date: PropSchema = {
|
||||
serializer: (jsDate: Date | null) =>
|
||||
jsDate != null ? jsDate.toISOString() : null,
|
||||
deserializer: (json: any, done) => {
|
||||
if (json === null) {
|
||||
return done(null, null);
|
||||
}
|
||||
try {
|
||||
done(null, new Date(json));
|
||||
} catch (e) {
|
||||
done(e, undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const dateOfYear: ModelSchema<s.DateOfYear> = {
|
||||
factory: () => new s.DateOfYear(),
|
||||
props: {
|
||||
year: primitive(),
|
||||
month:
|
||||
primitive(), // this only works if it is represented as a # from 0-12
|
||||
month: primitive(), // this only works if it is represented as a # from 0-12
|
||||
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 list from "./list";
|
||||
|
||||
@ -85,8 +85,7 @@ export const program: ModelSchema<s.Program> = {
|
||||
enabled: primitive(),
|
||||
schedule: object(schedule),
|
||||
sequence: list(object(programItem)),
|
||||
running: primitive(),
|
||||
nextRun: date(),
|
||||
running: primitive()
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {primitive, PropSchema} from 'serializr';
|
||||
import { primitive, PropSchema } from "serializr";
|
||||
|
||||
function invariant(cond: boolean, message?: string) {
|
||||
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) {
|
||||
return typeof propSchema === 'object' && !!propSchema.jsonname;
|
||||
return typeof propSchema === "object" && !!propSchema.jsonname;
|
||||
}
|
||||
|
||||
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) {
|
||||
return void cb(null, []);
|
||||
}
|
||||
@ -40,15 +43,17 @@ function parallel(
|
||||
|
||||
export default function list(propSchema: PropSchema): PropSchema {
|
||||
propSchema = propSchema || primitive();
|
||||
invariant(isPropSchema(propSchema), 'expected prop schema as first argument');
|
||||
invariant(isPropSchema(propSchema), "expected prop schema as first argument");
|
||||
invariant(
|
||||
!isAliasedPropSchema(propSchema),
|
||||
'provided prop is aliased, please put aliases first');
|
||||
!isAliasedPropSchema(propSchema),
|
||||
"provided prop is aliased, please put aliases first"
|
||||
);
|
||||
return {
|
||||
serializer(ar) {
|
||||
invariant(
|
||||
ar && typeof ar.length === 'number' && typeof ar.map === 'function',
|
||||
'expected array (like) object');
|
||||
ar && typeof ar.length === "number" && typeof ar.map === "function",
|
||||
"expected array (like) object"
|
||||
);
|
||||
return ar.map(propSchema.serializer);
|
||||
},
|
||||
deserializer(jsonArray, done, context) {
|
||||
@ -57,15 +62,14 @@ export default function list(propSchema: PropSchema): PropSchema {
|
||||
return void done(null, []);
|
||||
}
|
||||
if (!Array.isArray(jsonArray)) {
|
||||
return void done('[serializr] expected JSON array', null);
|
||||
return void done("[serializr] expected JSON array", null);
|
||||
}
|
||||
parallel(
|
||||
jsonArray,
|
||||
(item: any, itemDone: (err: any, targetPropertyValue: any) => void) =>
|
||||
propSchema.deserializer(item, itemDone, context, undefined),
|
||||
done);
|
||||
},
|
||||
beforeDeserialize: undefined as any,
|
||||
afterDeserialize: undefined as any,
|
||||
jsonArray,
|
||||
(item: any, itemDone: (err: any, targetPropertyValue: any) => void) =>
|
||||
propSchema.deserializer(item, itemDone, context, undefined),
|
||||
done
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -35,9 +35,7 @@ export const updateProgram: ModelSchema<
|
||||
serializer: data => data,
|
||||
deserializer: (json, done) => {
|
||||
done(null, json);
|
||||
},
|
||||
beforeDeserialize: undefined as any,
|
||||
afterDeserialize: undefined as any,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -14,7 +14,6 @@
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
],
|
||||
"exclude": []
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
@ -12,7 +12,6 @@ services:
|
||||
- "8080:8080"
|
||||
- "8081:8081"
|
||||
volumes:
|
||||
- ./bin:/app/bin
|
||||
- ./client:/app/client
|
||||
- ./common:/app/common
|
||||
- ./server:/app/server
|
||||
@ -37,13 +36,8 @@ services:
|
||||
- "1883:1883"
|
||||
|
||||
database:
|
||||
image: "postgres:11"
|
||||
image: "postgres:11-alpine"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- data-volume:/var/lib/postgres/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
||||
|
||||
volumes:
|
||||
data-volume:
|
@ -20,6 +20,6 @@ services:
|
||||
# Must specify JWT_SECRET and MQTT_URL
|
||||
|
||||
database:
|
||||
image: "postgres:11"
|
||||
image: "postgres:11-alpine"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
||||
|
146
package.json
146
package.json
@ -43,111 +43,115 @@
|
||||
"commands": "./dist/commands"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1.5.0",
|
||||
"@oclif/config": "^1.7.4",
|
||||
"@oclif/plugin-help": "^2.1.1",
|
||||
"bcrypt": "^3.0.0",
|
||||
"@oclif/command": "^1.5.6",
|
||||
"@oclif/config": "^1.9.0",
|
||||
"@oclif/plugin-help": "^2.1.4",
|
||||
"@types/split2": "^2.1.6",
|
||||
"bcrypt": "^3.0.2",
|
||||
"body-parser": "^1.18.3",
|
||||
"chalk": "^2.4.1",
|
||||
"cli-ux": "^5.3.1",
|
||||
"express": "^4.16.3",
|
||||
"cli-ux": "^4.9.3",
|
||||
"express": "^4.16.4",
|
||||
"express-pino-logger": "^4.0.0",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"globby": "^10.0.1",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"lodash": "^4.17.10",
|
||||
"mobx": "^5.1.0",
|
||||
"mobx-utils": "^5.0.1",
|
||||
"jsonwebtoken": "^8.4.0",
|
||||
"lodash": "^4.17.11",
|
||||
"mobx": "^5.7.0",
|
||||
"mobx-utils": "^5.1.0",
|
||||
"module-alias": "^2.1.0",
|
||||
"moment": "^2.22.2",
|
||||
"mqtt": "^3.0.0",
|
||||
"pg": "^7.4.3",
|
||||
"pino": "^5.4.0",
|
||||
"pino-http": "^4.2.0",
|
||||
"mqtt": "^2.18.8",
|
||||
"pg": "^7.7.1",
|
||||
"pino": "^5.10.0",
|
||||
"pump": "^3.0.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"serializr": "^1.3.0",
|
||||
"split2": "^3.0.0",
|
||||
"terser-webpack-plugin": "^1.3.0",
|
||||
"through2": "^3.0.1",
|
||||
"typeorm": "^0.2.7",
|
||||
"ws": "^7.1.1"
|
||||
"through2": "^3.0.0",
|
||||
"typeorm": "^0.2.9",
|
||||
"ws": "^6.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hot-loader/react-dom": "^16.8.6",
|
||||
"@types/async": "^3.0.0",
|
||||
"@types/async": "^2.0.50",
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/jsonwebtoken": "^8.3.2",
|
||||
"@types/lodash": "^4.14.116",
|
||||
"@types/jsonwebtoken": "^8.3.0",
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/module-alias": "^2.0.0",
|
||||
"@types/node": "^11.11.3",
|
||||
"@types/node": "^10.12.12",
|
||||
"@types/object-assign": "^4.0.30",
|
||||
"@types/pino": "^5.20.0",
|
||||
"@types/pino-http": "^4.0.2",
|
||||
"@types/prop-types": "^15.5.5",
|
||||
"@types/prop-types": "^15.5.7",
|
||||
"@types/pump": "^1.0.1",
|
||||
"@types/query-string": "^6.1.0",
|
||||
"@types/react": "^16.7.13",
|
||||
"@types/react-dom": "^16.0.11",
|
||||
"@types/query-string": "^6.1.1",
|
||||
"@types/react": "16.7.13",
|
||||
"@types/react-dom": "16.0.11",
|
||||
"@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/split2": "^2.1.6",
|
||||
"@types/through2": "^2.0.33",
|
||||
"@types/through2": "^2.0.34",
|
||||
"@types/webpack-env": "^1.13.6",
|
||||
"@types/ws": "^6.0.0",
|
||||
"async": "^3.1.0",
|
||||
"autoprefixer": "^9.1.3",
|
||||
"cache-loader": "^4.1.0",
|
||||
"@types/ws": "^6.0.1",
|
||||
"async": "^2.6.1",
|
||||
"autoprefixer": "^9.4.2",
|
||||
"cache-loader": "^1.2.5",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"classnames": "^2.2.6",
|
||||
"css-loader": "^3.1.0",
|
||||
"dotenv": "^8.0.0",
|
||||
"css-loader": "^2.0.0",
|
||||
"dotenv": "^6.2.0",
|
||||
"favicons-webpack-plugin": "^0.0.9",
|
||||
"file-loader": "^4.1.0",
|
||||
"file-loader": "^2.0.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",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"mobx-react": "^6.1.1",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"mobx-react": "^5.4.2",
|
||||
"mobx-react-devtools": "^6.0.3",
|
||||
"mobx-react-router": "^4.0.4",
|
||||
"node-sass": "^4.9.3",
|
||||
"nodemon": "^1.18.4",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"mobx-react-router": "^4.0.5",
|
||||
"node-sass": "^4.11.0",
|
||||
"nodemon": "^1.18.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"object-assign": "^4.1.1",
|
||||
"postcss-flexbugs-fixes": "^4.1.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"promise": "^8.0.1",
|
||||
"postcss-preset-env": "^6.4.0",
|
||||
"promise": "^8.0.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"query-string": "^6.1.0",
|
||||
"react": "^16.8.0",
|
||||
"react-dev-utils": "^9.0.1",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-hot-loader": "^4.3.5",
|
||||
"react-router": "^5.0.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-sortable-hoc": "^1.9.1",
|
||||
"query-string": "^6.2.0",
|
||||
"react": "16.6.3",
|
||||
"react-dev-utils": "^6.1.1",
|
||||
"react-dom": "16.6.3",
|
||||
"react-hot-loader": "^4.3.12",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-sortable-hoc": "^0.8.4",
|
||||
"sass-loader": "^7.1.0",
|
||||
"semantic-ui-css": "^2.3.3",
|
||||
"semantic-ui-react": "^0.87.3",
|
||||
"semantic-ui-css": "^2.4.1",
|
||||
"semantic-ui-react": "^0.84.0",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"style-loader": "^0.23.0",
|
||||
"thread-loader": "^2.1.2",
|
||||
"ts-loader": "^6.0.4",
|
||||
"style-loader": "^0.23.1",
|
||||
"thread-loader": "^1.2.0",
|
||||
"ts-loader": "^5.3.1",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-config-prettier": "^1.15.0",
|
||||
"tslint-consistent-codestyle": "^1.13.3",
|
||||
"tslint-react": "^4.0.0",
|
||||
"typescript": "^3.0.3",
|
||||
"url-loader": "^2.1.0",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dashboard": "^3.0.7",
|
||||
"webpack-dev-server": "^3.1.7"
|
||||
"tslint-config-prettier": "^1.17.0",
|
||||
"tslint-consistent-codestyle": "^1.14.1",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^3.2.2",
|
||||
"uglify-es": "^3.3.9",
|
||||
"uglifyjs-webpack-plugin": "^2.0.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.27.1",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"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 { SprinklersDevice, User } from "./entities";
|
||||
import { User } from "./entities";
|
||||
import { SprinklersDeviceRepository, UserRepository } from "./repositories/";
|
||||
|
||||
export class Database {
|
||||
@ -24,30 +24,25 @@ export class Database {
|
||||
Object.assign(options, {
|
||||
entities: [path.resolve(__dirname, "entities", "*.js")]
|
||||
});
|
||||
if (options.synchronize) {
|
||||
logger.warn("synchronizing database schema");
|
||||
}
|
||||
this._conn = await createConnection(options);
|
||||
this.users = this._conn.getCustomRepository(UserRepository);
|
||||
this.sprinklersDevices = this._conn.getCustomRepository(
|
||||
SprinklersDeviceRepository
|
||||
);
|
||||
logger.info("connected to database");
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this._conn) {
|
||||
await this._conn.close();
|
||||
logger.info("disconnected from database");
|
||||
return this._conn.close();
|
||||
}
|
||||
}
|
||||
|
||||
async insertTestData() {
|
||||
const NUM = 50;
|
||||
const NUM = 100;
|
||||
const users: User[] = [];
|
||||
for (let i = 0; i < NUM; i++) {
|
||||
const username = "alex" + i;
|
||||
let user = await this.users.findByUsername(username, { devices: true });
|
||||
let user = await this.users.findByUsername(username);
|
||||
if (!user) {
|
||||
user = await this.users.create({
|
||||
name: "Alex Mikhalev" + i,
|
||||
@ -58,8 +53,6 @@ export class Database {
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
const devices: SprinklersDevice[] = [];
|
||||
|
||||
for (let i = 0; i < NUM; i++) {
|
||||
const name = "Test" + i;
|
||||
let device = await this.sprinklersDevices.findByName(name);
|
||||
@ -70,17 +63,13 @@ export class Database {
|
||||
name,
|
||||
deviceId: "grinklers" + (i === 1 ? "" : i)
|
||||
});
|
||||
devices.push(device);
|
||||
await this.sprinklersDevices.save(device);
|
||||
for (let j = 0; j < 5; j++) {
|
||||
const userIdx = (i + j * 10) % NUM;
|
||||
const user = users[userIdx];
|
||||
if (!user.devices) {
|
||||
user.devices = [];
|
||||
}
|
||||
user.devices.push(device);
|
||||
user.devices = (user.devices || []).concat([device]);
|
||||
}
|
||||
}
|
||||
await this.sprinklersDevices.save(devices);
|
||||
logger.info("inserted/updated devices");
|
||||
|
||||
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 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(
|
||||
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()
|
||||
name: string = "";
|
||||
|
||||
@ManyToMany(type => User, user => user.devices)
|
||||
@ManyToMany(type => User)
|
||||
users: User[] | undefined;
|
||||
|
||||
constructor(data?: Partial<SprinklersDevice>) {
|
||||
|
@ -3,7 +3,6 @@ import { omit } from "lodash";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
PrimaryGeneratedColumn
|
||||
@ -19,8 +18,7 @@ export class User implements IUser {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
@Index("user_username_unique", { unique: true })
|
||||
@Column({ unique: true })
|
||||
username: string = "";
|
||||
|
||||
@Column()
|
||||
@ -29,7 +27,7 @@ export class User implements IUser {
|
||||
@Column()
|
||||
passwordHash: string = "";
|
||||
|
||||
@ManyToMany(type => SprinklersDevice, device => device.users)
|
||||
@ManyToMany(type => SprinklersDevice)
|
||||
@JoinTable({ name: "user_sprinklers_device" })
|
||||
devices: SprinklersDevice[] | undefined;
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { Request } from "express";
|
||||
import PromiseRouter from "express-promise-router";
|
||||
import { serialize } from "serializr";
|
||||
|
||||
import ApiError from "@common/ApiError";
|
||||
import { ErrorCode } from "@common/ErrorCode";
|
||||
import * as schema from "@common/sprinklersRpc/schema";
|
||||
import { DeviceToken } from "@common/TokenClaims";
|
||||
import { generateDeviceToken } from "@server/authentication";
|
||||
import { SprinklersDevice } from "@server/entities";
|
||||
import { verifyAuthorization } from "@server/express/verifyAuthorization";
|
||||
import { ServerState } from "@server/state";
|
||||
|
||||
@ -33,7 +30,7 @@ function randomDeviceId(): string {
|
||||
export function devices(state: ServerState) {
|
||||
const router = PromiseRouter();
|
||||
|
||||
async function verifyUserDevice(req: Request): Promise<SprinklersDevice> {
|
||||
router.get("/:deviceId", verifyAuthorization(), async (req, res) => {
|
||||
const token = req.token!;
|
||||
const userId = token.aud;
|
||||
const deviceId = req.params.deviceId;
|
||||
@ -47,39 +44,12 @@ export function devices(state: ServerState) {
|
||||
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 j = serialize(schema.sprinklersDevice, device);
|
||||
res.send(j);
|
||||
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(
|
||||
"/register",
|
||||
verifyAuthorization({
|
||||
@ -106,19 +76,8 @@ export function devices(state: ServerState) {
|
||||
type: "device"
|
||||
}),
|
||||
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({
|
||||
mqttUrl: state.mqttUrl,
|
||||
deviceId,
|
||||
clientId,
|
||||
url: state.mqttUrl
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import log from "@common/logger";
|
||||
import pinoHttp = require("pino-http");
|
||||
import expressPinoLogger = require("express-pino-logger");
|
||||
import * as pino from "pino";
|
||||
|
||||
export default pinoHttp({
|
||||
logger: log,
|
||||
useLevel: "debug",
|
||||
} as pinoHttp.Options);
|
||||
const l = pino();
|
||||
pino(l);
|
||||
|
||||
export default expressPinoLogger(log);
|
||||
|
@ -5,6 +5,5 @@ import "./env";
|
||||
import "./configureLogger";
|
||||
|
||||
export { ServerState } from "./state";
|
||||
export { Database } from "./Database";
|
||||
export { createApp } from "./express";
|
||||
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 UniqueConstraintError from "@server/UniqueConstraintError";
|
||||
|
||||
@EntityRepository(SprinklersDevice)
|
||||
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
||||
@ -40,17 +39,4 @@ export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
||||
}
|
||||
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";
|
||||
|
||||
export interface FindUserOptions extends FindOneOptions<User> {
|
||||
export interface FindUserOptions {
|
||||
devices: boolean;
|
||||
}
|
||||
|
||||
function computeOptions(
|
||||
function applyDefaultOptions(
|
||||
options?: Partial<FindUserOptions>
|
||||
): FindOneOptions<User> {
|
||||
const opts: FindUserOptions = { devices: false, ...options };
|
||||
const { devices, ...rest } = opts;
|
||||
const relations = [devices && "devices"].filter(Boolean) as string[];
|
||||
return { relations, ...rest };
|
||||
const relations = [opts.devices && "devices"].filter(Boolean) as string[];
|
||||
return { relations };
|
||||
}
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
findAll(options?: Partial<FindUserOptions>) {
|
||||
const opts = computeOptions(options);
|
||||
const opts = applyDefaultOptions(options);
|
||||
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>) {
|
||||
return this.findOneUser({ id }, options);
|
||||
const opts = applyDefaultOptions(options);
|
||||
return super.findOne(id, opts);
|
||||
}
|
||||
|
||||
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() {
|
||||
await this.database.connect();
|
||||
logger.info("connected to database");
|
||||
|
||||
if (process.env.INSERT_TEST_DATA) {
|
||||
try {
|
||||
await this.database.insertTestData();
|
||||
logger.info("inserted test data");
|
||||
} catch (e) {
|
||||
logger.error(e, "error inserting test data");
|
||||
}
|
||||
await this.database.insertTestData();
|
||||
logger.info("inserted test data");
|
||||
}
|
||||
}
|
||||
|
||||
async stopDatabase() {
|
||||
await this.database.disconnect();
|
||||
}
|
||||
|
||||
async startMqtt() {
|
||||
this.mqttClient.username = SUPERUSER;
|
||||
|
@ -2,25 +2,24 @@
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist",
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"baseUrl": "..",
|
||||
"paths": {
|
||||
"@common/*": [
|
||||
"./common/*"
|
||||
],
|
||||
"@server/*": [
|
||||
"./server/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"references": [{
|
||||
"path": "../common"
|
||||
}],
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
// "sourceMap": true,
|
||||
// "emitDecoratorMetadata": true,
|
||||
// "strict": true,
|
||||
// "allowJs": true,
|
||||
// "baseUrl": "..",
|
||||
// "paths": {
|
||||
// "@common/*": [
|
||||
// "./common/*"
|
||||
// ],
|
||||
// "@server/*": [
|
||||
// "./server/*"
|
||||
// ]
|
||||
// }
|
||||
}//,
|
||||
// "references": [{
|
||||
// "path": "../common"
|
||||
// }],
|
||||
// "include": [
|
||||
// "./**/*.ts"
|
||||
// ]
|
||||
}
|
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": [
|
||||
"node"
|
||||
],
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@common/*": [
|
||||
"./common/*"
|
||||
],
|
||||
"@client/*": [
|
||||
"./client/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"./client",
|
||||
"./common",
|
||||
"./server"
|
||||
]
|
||||
"strict": true
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user