diff --git a/app/components/DeviceView.tsx b/app/components/DeviceView.tsx index 4647b1c..9641f28 100644 --- a/app/components/DeviceView.tsx +++ b/app/components/DeviceView.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react"; import { injectState, StateBase } from "@app/state"; -import { ConnectionState as ConState } from "@common/sprinklers"; +import { ConnectionState as ConState } from "@common/sprinklersRpc"; import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from "."; import "./DeviceView.scss"; diff --git a/app/components/ProgramTable.tsx b/app/components/ProgramTable.tsx index 7401786..0b6c2d1 100644 --- a/app/components/ProgramTable.tsx +++ b/app/components/ProgramTable.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { Button, Table } from "semantic-ui-react"; import { Duration } from "@common/Duration"; -import { DateOfYear, Program, Schedule, Section, TimeOfDay, Weekday } from "@common/sprinklers"; +import { DateOfYear, Program, Schedule, Section, TimeOfDay, Weekday } from "@common/sprinklersRpc"; function timeToString(time: TimeOfDay) { return moment(time).format("LTS"); diff --git a/app/components/RunSectionForm.tsx b/app/components/RunSectionForm.tsx index 4bc0a3c..380553f 100644 --- a/app/components/RunSectionForm.tsx +++ b/app/components/RunSectionForm.tsx @@ -6,8 +6,8 @@ import { DropdownItemProps, DropdownProps, Form, Header, Segment } from "semanti import { UiStore } from "@app/state"; import { Duration } from "@common/Duration"; import log from "@common/logger"; -import { Section, SprinklersDevice } from "@common/sprinklers"; -import { RunSectionResponse } from "@common/sprinklers/deviceRequests"; +import { Section, SprinklersDevice } from "@common/sprinklersRpc"; +import { RunSectionResponse } from "@common/sprinklersRpc/deviceRequests"; import DurationInput from "./DurationInput"; @observer diff --git a/app/components/SectionRunnerView.tsx b/app/components/SectionRunnerView.tsx index 163bab4..64b0590 100644 --- a/app/components/SectionRunnerView.tsx +++ b/app/components/SectionRunnerView.tsx @@ -5,7 +5,7 @@ import { Button, Icon, Progress, Segment } from "semantic-ui-react"; import { Duration } from "@common/Duration"; import log from "@common/logger"; -import { Section, SectionRun, SectionRunner } from "@common/sprinklers"; +import { Section, SectionRun, SectionRunner } from "@common/sprinklersRpc"; interface PausedStateProps { paused: boolean; diff --git a/app/components/SectionTable.tsx b/app/components/SectionTable.tsx index 972611e..bc16ee7 100644 --- a/app/components/SectionTable.tsx +++ b/app/components/SectionTable.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Icon, Table } from "semantic-ui-react"; -import { Section } from "@common/sprinklers"; +import { Section } from "@common/sprinklersRpc"; /* tslint:disable:object-literal-sort-keys */ diff --git a/app/index.tsx b/app/index.tsx index 2c237b1..06770de 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -3,11 +3,14 @@ import * as ReactDOM from "react-dom"; import { AppContainer } from "react-hot-loader"; import App from "@app/components/App"; -import { ProvideState, StateBase } from "@app/state"; -import { WebApiState as StateClass } from "@app/state/web"; +import { ProvideState, StateBase, WebApiState as StateClass } from "@app/state"; +import logger from "@common/logger"; const state: StateBase = new StateClass(); -state.start(); +state.start() + .catch((err) => { + logger.error({err}, "error starting state"); + }); const rootElem = document.getElementById("app"); diff --git a/app/sprinklers/websocket.ts b/app/sprinklersRpc/websocketClient.ts similarity index 92% rename from app/sprinklers/websocket.ts rename to app/sprinklersRpc/websocketClient.ts index 41447a3..82cedcc 100644 --- a/app/sprinklers/websocket.ts +++ b/app/sprinklersRpc/websocketClient.ts @@ -3,12 +3,12 @@ import { update } from "serializr"; import * as rpc from "@common/jsonRpc"; import logger from "@common/logger"; -import * as deviceRequests from "@common/sprinklers/deviceRequests"; -import { ErrorCode } from "@common/sprinklers/ErrorCode"; -import * as s from "@common/sprinklers/index"; -import * as schema from "@common/sprinklers/schema/index"; -import { seralizeRequest } from "@common/sprinklers/schema/requests"; -import * as ws from "@common/sprinklers/websocketData"; +import * as deviceRequests from "@common/sprinklersRpc/deviceRequests"; +import { ErrorCode } from "@common/sprinklersRpc/ErrorCode"; +import * as s from "@common/sprinklersRpc/index"; +import * as schema from "@common/sprinklersRpc/schema/index"; +import { seralizeRequest } from "@common/sprinklersRpc/schema/requests"; +import * as ws from "@common/sprinklersRpc/websocketData"; const log = logger.child({ source: "websocket" }); @@ -36,7 +36,9 @@ export class WSSprinklersDevice extends s.SprinklersDevice { } async subscribe() { - await this.api.authenticate("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzcHJpbmtsZXJzMyIsImF1ZCI6IjA4NDQ4N2Q1LWU1NzktNDQ5YS05MzI5LTU5NWJlNGJjMmJiYyIsIm5hbWUiOiJBbGV4IE1pa2hhbGV2IiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTUzMDQxNzU3MCwiaWF0IjoxNTMwNDE1NzcwfQ.fRGiN_X1j3Hwe8a5y68wXLx1DQPtTkQr9h6Uh848dFM"); + if (this.api.accessToken) { + await this.api.authenticate(this.api.accessToken); + } const subscribeRequest: ws.IDeviceSubscribeRequest = { deviceId: this.id, }; @@ -58,7 +60,7 @@ export class WSSprinklersDevice extends s.SprinklersDevice { } } -export class WebSocketApiClient implements s.ISprinklersApi { +export class WebSocketApiClient implements s.SprinklersRPC { readonly webSocketUrl: string; devices: Map = new Map(); @@ -68,6 +70,7 @@ export class WebSocketApiClient implements s.ISprinklersApi { private nextRequestId = Math.round(Math.random() * 1000000); private responseCallbacks: ws.ServerResponseHandlers = {}; private reconnectTimer: number | null = null; + accessToken: string | undefined; get connected(): boolean { return this.connectionState.isConnected || false; diff --git a/app/state/ClientState.ts b/app/state/ClientState.ts new file mode 100644 index 0000000..3100788 --- /dev/null +++ b/app/state/ClientState.ts @@ -0,0 +1,26 @@ +import { WebSocketApiClient } from "@app/sprinklersRpc/websocketClient"; +import HttpApi from "@app/state/HttpApi"; +import { UiStore } from "@app/state/UiStore"; + +const isDev = process.env.NODE_ENV === "development"; +const websocketPort = isDev ? 8080 : location.port; + +export default class ClientState { + sprinklersApi = new WebSocketApiClient(`ws://${location.hostname}:${websocketPort}`); + uiStore = new UiStore(); + httpApi = new HttpApi(); + + async start() { + if (!this.httpApi.tokenStore.accessToken.isValid) { + if (this.httpApi.tokenStore.refreshToken.isValid) { + await this.httpApi.tokenStore.grantRefresh(); + } else { + await this.httpApi.tokenStore.grantPassword("alex", "kakashka"); + } + } + + this.sprinklersApi.accessToken = this.httpApi.tokenStore.accessToken.token!; + + this.sprinklersApi.start(); + } +} diff --git a/app/state/HttpApi.ts b/app/state/HttpApi.ts new file mode 100644 index 0000000..260e173 --- /dev/null +++ b/app/state/HttpApi.ts @@ -0,0 +1,54 @@ +import { Token } from "@app/state/Token"; +import { TokenStore } from "@app/state/TokenStore"; + +export class HttpApiError extends Error { + name = "HttpApiError"; + status: number; + + constructor(message: string, status: number = 500) { + super(message); + this.status = status; + } +} + +export default class HttpApi { + baseUrl: string; + + tokenStore: TokenStore; + + private get authorizationHeader(): {} | { "Authorization": string } { + if (!this.tokenStore.accessToken) { + return {}; + } + return { Authorization: `Bearer ${this.tokenStore.accessToken.token}` }; + } + + constructor(baseUrl: string = `http://${location.hostname}:${location.port}/api`) { + while (baseUrl.charAt(baseUrl.length - 1) === "/") { + baseUrl = baseUrl.substring(0, baseUrl.length - 1); + } + this.baseUrl = baseUrl; + + this.tokenStore = new TokenStore(this); + } + + async makeRequest(url: string, options?: RequestInit, body?: any): Promise { + options = options || {}; + options = { + headers: { + "Content-Type": "application/json", + ...this.authorizationHeader, + ...options.headers || {}, + }, + body: JSON.stringify(body), + ...options, + }; + const response = await fetch(this.baseUrl + url, options); + const responseBody = await response.json() || {}; + if (!response.ok) { + throw new HttpApiError(responseBody.message || response.statusText, response.status); + } + return responseBody; + } + +} diff --git a/app/state/StateBase.ts b/app/state/StateBase.ts deleted file mode 100644 index 5fdf578..0000000 --- a/app/state/StateBase.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ISprinklersApi } from "@common/sprinklers"; -import { UiStore } from "./UiStore"; - -export default abstract class StateBase { - abstract readonly sprinklersApi: ISprinklersApi; - uiStore = new UiStore(); - - start() { - this.sprinklersApi.start(); - } -} diff --git a/app/state/Token.ts b/app/state/Token.ts new file mode 100644 index 0000000..e98d75d --- /dev/null +++ b/app/state/Token.ts @@ -0,0 +1,65 @@ +import TokenClaims from "@common/TokenClaims"; +import * as jwt from "jsonwebtoken"; +import { computed, createAtom, IAtom, observable } from "mobx"; + +export class Token { + @observable token: string | null; + + @computed get claims(): TokenClaims | null { + if (this.token == null) { + return null; + } + return jwt.decode(this.token) as any; + } + + private isExpiredAtom: IAtom; + private currentTime!: number; + private expirationTimer: number | undefined; + + constructor(token: string | null = null) { + this.token = token; + this.isExpiredAtom = createAtom("Token.isExpired", + this.startUpdating, this.stopUpdating); + this.updateCurrentTime(); + } + + private updateCurrentTime = (reportChanged: boolean = true) => { + if (reportChanged) { + this.isExpiredAtom.reportChanged(); + } + this.currentTime = Date.now() / 1000; + } + + get remainingTime(): number { + if (!this.isExpiredAtom.reportObserved()) { + this.updateCurrentTime(false); + } + if (this.claims == null) { + return Number.NEGATIVE_INFINITY; + } + return this.claims.exp - this.currentTime; + } + + private startUpdating = () => { + this.stopUpdating(); + const remaining = this.remainingTime; + if (remaining > 0) { + this.expirationTimer = setTimeout(this.updateCurrentTime, this.remainingTime); + } + } + + private stopUpdating = () => { + if (this.expirationTimer != null) { + clearTimeout(this.expirationTimer); + this.expirationTimer = undefined; + } + } + + get isExpired() { + return this.remainingTime <= 0; + } + + @computed get isValid() { + return this.token != null && !this.isExpired; + } +} \ No newline at end of file diff --git a/app/state/TokenStore.ts b/app/state/TokenStore.ts new file mode 100644 index 0000000..71db5eb --- /dev/null +++ b/app/state/TokenStore.ts @@ -0,0 +1,45 @@ +import { observable } from "mobx"; + +import HttpApi, { HttpApiError } from "@app/state/HttpApi"; +import { Token } from "@app/state/Token"; +import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/http"; +import logger from "@common/logger"; + +export class TokenStore { + @observable accessToken: Token = new Token(); + @observable refreshToken: Token = new Token(); + + private api: HttpApi; + + constructor(api: HttpApi) { + this.api = api; + } + + async grantPassword(username: string, password: string) { + const request: TokenGrantPasswordRequest = { + grant_type: "password", username, password, + }; + const response: TokenGrantResponse = await this.api.makeRequest("/token/grant", { + method: "POST", + }, request); + this.accessToken.token = response.access_token; + this.refreshToken.token = response.refresh_token; + logger.debug({ aud: this.accessToken.claims!.aud }, "got password grant tokens"); + } + + async grantRefresh() { + if (!this.refreshToken.isValid) { + throw new HttpApiError("can not grant refresh with invalid refresh_token"); + } + const request: TokenGrantRefreshRequest = { + grant_type: "refresh", refresh_token: this.refreshToken.token!, + }; + const response: TokenGrantResponse = await this.api.makeRequest("/token/grant", { + method: "POST", + }, request); + this.accessToken.token = response.access_token; + this.refreshToken.token = response.refresh_token; + logger.debug({ aud: this.accessToken.claims!.aud }, "got refresh grant tokens"); + } +} + diff --git a/app/state/index.ts b/app/state/index.ts index 522e323..0c806fc 100644 --- a/app/state/index.ts +++ b/app/state/index.ts @@ -1,3 +1,9 @@ export { UiMessage, UiStore } from "./UiStore"; export * from "./reactContext"; -export { default as StateBase } from "./StateBase"; +export { ClientState as StateBase } from "./ClientState"; + +import ClientState from "./ClientState"; + + +export class WebApiState extends ClientState { +} diff --git a/app/state/web.ts b/app/state/web.ts deleted file mode 100644 index df30e6e..0000000 --- a/app/state/web.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { MqttApiClient } from "@common/sprinklers/mqtt"; -import { WebSocketApiClient } from "../sprinklers/websocket"; -import StateBase from "./StateBase"; - -const isDev = process.env.NODE_ENV === "development"; -const websocketPort = isDev ? 8080 : location.port; - -export class MqttApiState extends StateBase { - sprinklersApi = new MqttApiClient(`ws://${location.hostname}:1884`); -} - -export class WebApiState extends StateBase { - sprinklersApi = new WebSocketApiClient(`ws://${location.hostname}:${websocketPort}`); -} diff --git a/common/TokenClaims.ts b/common/TokenClaims.ts new file mode 100644 index 0000000..532c04b --- /dev/null +++ b/common/TokenClaims.ts @@ -0,0 +1,7 @@ +export default interface TokenClaims { + iss: string; + type: "access" | "refresh"; + aud: string; + name: string; + exp: number; +} diff --git a/common/http.ts b/common/http.ts new file mode 100644 index 0000000..899eccf --- /dev/null +++ b/common/http.ts @@ -0,0 +1,17 @@ +export interface TokenGrantPasswordRequest { + grant_type: "password"; + username: string; + password: string; +} + +export interface TokenGrantRefreshRequest { + grant_type: "refresh"; + refresh_token: string; +} + +export type TokenGrantRequest = TokenGrantPasswordRequest | TokenGrantRefreshRequest; + +export interface TokenGrantResponse { + access_token: string; + refresh_token: string; +} \ No newline at end of file diff --git a/common/sprinklers/ConnectionState.ts b/common/sprinklersRpc/ConnectionState.ts similarity index 100% rename from common/sprinklers/ConnectionState.ts rename to common/sprinklersRpc/ConnectionState.ts diff --git a/common/sprinklers/ErrorCode.ts b/common/sprinklersRpc/ErrorCode.ts similarity index 100% rename from common/sprinklers/ErrorCode.ts rename to common/sprinklersRpc/ErrorCode.ts diff --git a/common/sprinklers/Program.ts b/common/sprinklersRpc/Program.ts similarity index 100% rename from common/sprinklers/Program.ts rename to common/sprinklersRpc/Program.ts diff --git a/common/sprinklers/Section.ts b/common/sprinklersRpc/Section.ts similarity index 100% rename from common/sprinklers/Section.ts rename to common/sprinklersRpc/Section.ts diff --git a/common/sprinklers/SectionRunner.ts b/common/sprinklersRpc/SectionRunner.ts similarity index 100% rename from common/sprinklers/SectionRunner.ts rename to common/sprinklersRpc/SectionRunner.ts diff --git a/common/sprinklers/SprinklersDevice.ts b/common/sprinklersRpc/SprinklersDevice.ts similarity index 100% rename from common/sprinklers/SprinklersDevice.ts rename to common/sprinklersRpc/SprinklersDevice.ts diff --git a/common/sprinklers/ISprinklersApi.ts b/common/sprinklersRpc/SprinklersRPC.ts similarity index 89% rename from common/sprinklers/ISprinklersApi.ts rename to common/sprinklersRpc/SprinklersRPC.ts index 79f44d6..938e55d 100644 --- a/common/sprinklers/ISprinklersApi.ts +++ b/common/sprinklersRpc/SprinklersRPC.ts @@ -1,7 +1,7 @@ import { ConnectionState } from "./ConnectionState"; import { SprinklersDevice } from "./SprinklersDevice"; -export interface ISprinklersApi { +export interface SprinklersRPC { readonly connectionState: ConnectionState; readonly connected: boolean; diff --git a/common/sprinklers/deviceRequests.ts b/common/sprinklersRpc/deviceRequests.ts similarity index 100% rename from common/sprinklers/deviceRequests.ts rename to common/sprinklersRpc/deviceRequests.ts diff --git a/common/sprinklers/index.ts b/common/sprinklersRpc/index.ts similarity index 86% rename from common/sprinklers/index.ts rename to common/sprinklersRpc/index.ts index 1834ce8..b615c38 100644 --- a/common/sprinklers/index.ts +++ b/common/sprinklersRpc/index.ts @@ -1,5 +1,5 @@ // export * from "./Duration"; -export * from "./ISprinklersApi"; +export * from "./SprinklersRPC"; export * from "./Program"; export * from "./schedule"; export * from "./Section"; diff --git a/common/sprinklers/mqtt/index.ts b/common/sprinklersRpc/mqtt/index.ts similarity index 97% rename from common/sprinklers/mqtt/index.ts rename to common/sprinklersRpc/mqtt/index.ts index 5956071..7a32d9d 100644 --- a/common/sprinklers/mqtt/index.ts +++ b/common/sprinklersRpc/mqtt/index.ts @@ -3,10 +3,10 @@ import * as mqtt from "mqtt"; import { update } from "serializr"; import logger from "@common/logger"; -import * as s from "@common/sprinklers"; -import * as requests from "@common/sprinklers/deviceRequests"; -import * as schema from "@common/sprinklers/schema"; -import { seralizeRequest } from "@common/sprinklers/schema/requests"; +import * as s from "@common/sprinklersRpc"; +import * as requests from "@common/sprinklersRpc/deviceRequests"; +import * as schema from "@common/sprinklersRpc/schema"; +import { seralizeRequest } from "@common/sprinklersRpc/schema/requests"; const log = logger.child({ source: "mqtt" }); @@ -14,7 +14,7 @@ interface WithRid { rid: number; } -export class MqttApiClient implements s.ISprinklersApi { +export class MqttApiClient implements s.SprinklersRPC { readonly mqttUri: string; client!: mqtt.Client; @observable connectionState: s.ConnectionState = new s.ConnectionState(); diff --git a/common/sprinklers/schedule.ts b/common/sprinklersRpc/schedule.ts similarity index 100% rename from common/sprinklers/schedule.ts rename to common/sprinklersRpc/schedule.ts diff --git a/common/sprinklers/schema/common.ts b/common/sprinklersRpc/schema/common.ts similarity index 100% rename from common/sprinklers/schema/common.ts rename to common/sprinklersRpc/schema/common.ts diff --git a/common/sprinklers/schema/index.ts b/common/sprinklersRpc/schema/index.ts similarity index 100% rename from common/sprinklers/schema/index.ts rename to common/sprinklersRpc/schema/index.ts diff --git a/common/sprinklers/schema/list.ts b/common/sprinklersRpc/schema/list.ts similarity index 100% rename from common/sprinklers/schema/list.ts rename to common/sprinklersRpc/schema/list.ts diff --git a/common/sprinklers/schema/requests.ts b/common/sprinklersRpc/schema/requests.ts similarity index 100% rename from common/sprinklers/schema/requests.ts rename to common/sprinklersRpc/schema/requests.ts diff --git a/common/sprinklers/websocketData.ts b/common/sprinklersRpc/websocketData.ts similarity index 95% rename from common/sprinklers/websocketData.ts rename to common/sprinklersRpc/websocketData.ts index 0a7c9e5..6ff33c1 100644 --- a/common/sprinklers/websocketData.ts +++ b/common/sprinklersRpc/websocketData.ts @@ -1,7 +1,7 @@ import * as rpc from "../jsonRpc/index"; -import { Response as ResponseData } from "@common/sprinklers/deviceRequests"; -import { ErrorCode } from "@common/sprinklers/ErrorCode"; +import { Response as ResponseData } from "@common/sprinklersRpc/deviceRequests"; +import { ErrorCode } from "@common/sprinklersRpc/ErrorCode"; export interface IAuthenticateRequest { accessToken: string; diff --git a/package.json b/package.json index e582eff..5b68754 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "fork-ts-checker-webpack-plugin": "^0.4.2", "jsonwebtoken": "^8.3.0", "mobx": "^5.0.3", + "mobx-utils": "^5.0.0", "module-alias": "^2.1.0", "moment": "^2.22.2", "mqtt": "^2.18.1", diff --git a/server/express/authentication.ts b/server/express/authentication.ts index aab3f2c..2aee1ef 100644 --- a/server/express/authentication.ts +++ b/server/express/authentication.ts @@ -1,9 +1,20 @@ import * as Express from "express"; import Router from "express-promise-router"; import * as jwt from "jsonwebtoken"; + +import TokenClaims from "@common/TokenClaims"; + import { User } from "../models/User"; import { ServerState } from "../state"; import { ApiError } from "./errors"; +import { + TokenGrantPasswordRequest, + TokenGrantRefreshRequest, + TokenGrantRequest, + TokenGrantResponse +} from "@common/http"; + +export { TokenClaims }; declare global { namespace Express { @@ -28,14 +39,6 @@ function getExpTime(lifetime: number) { return Math.floor(Date.now() / 1000) + lifetime; } -export interface TokenClaims { - iss: string; - type: "access" | "refresh"; - aud: string; - name: string; - exp: number; -} - function signToken(claims: TokenClaims): Promise { return new Promise((resolve, reject) => { jwt.sign(claims, JWT_SECRET, (err: Error, encoded: string) => { @@ -94,8 +97,7 @@ export function authentication(state: ServerState) { const router = Router(); - async function passwordGrant(req: Express.Request, res: Express.Response) { - const { body } = req; + async function passwordGrant(body: TokenGrantPasswordRequest, res: Express.Response): Promise { const { username, password } = body; if (!body || !username || !password) { throw new ApiError(400, "Must specify username and password"); @@ -106,22 +108,13 @@ export function authentication(state: ServerState) { } const passwordMatches = user.comparePassword(password); if (passwordMatches) { - const [access_token, refresh_token] = await Promise.all( - [await generateAccessToken(user, JWT_SECRET), - await generateRefreshToken(user, JWT_SECRET)]); - res.json({ - access_token, refresh_token, - }); + return user; } else { - res.status(400) - .json({ - message: "incorrect login", - }); + throw new ApiError(400, "User does not exist"); } } - async function refreshGrant(req: Express.Request, res: Express.Response) { - const { body } = req; + async function refreshGrant(body: TokenGrantRefreshRequest, res: Express.Response): Promise { const { refresh_token } = body; if (!body || !refresh_token) { throw new ApiError(400, "Must specify a refresh_token"); @@ -134,24 +127,26 @@ export function authentication(state: ServerState) { if (!user) { throw new ApiError(400, "User does not exist"); } - const [access_token, new_refresh_token] = await Promise.all( - [await generateAccessToken(user, JWT_SECRET), - await generateRefreshToken(user, JWT_SECRET)]); - res.json({ - access_token, refresh_token: new_refresh_token, - }); + return user; } router.post("/token/grant", async (req, res) => { - const { body } = req; - const { grant_type } = body; - if (grant_type === "password") { - await passwordGrant(req, res); - } else if (grant_type === "refresh") { - await refreshGrant(req, res); + const body: TokenGrantRequest = req.body; + let user: User; + if (body.grant_type === "password") { + user = await passwordGrant(body, res); + } else if (body.grant_type === "refresh") { + user = await refreshGrant(body, res); } else { throw new ApiError(400, "Invalid grant_type"); } + const [access_token, refresh_token] = await Promise.all( + [await generateAccessToken(user, JWT_SECRET), + await generateRefreshToken(user, JWT_SECRET)]); + const response: TokenGrantResponse = { + access_token, refresh_token, + }; + res.json(response); }); router.post("/token/verify", authorizeAccess, async (req, res) => { diff --git a/server/express/index.ts b/server/express/index.ts index 6d0db7b..3c86d12 100644 --- a/server/express/index.ts +++ b/server/express/index.ts @@ -2,7 +2,7 @@ import * as bodyParser from "body-parser"; import * as express from "express"; import { serialize} from "serializr"; -import * as schema from "@common/sprinklers/schema"; +import * as schema from "@common/sprinklersRpc/schema"; import { ServerState } from "../state"; import logger from "./logger"; import serveApp from "./serveApp"; diff --git a/server/index.ts b/server/index.ts index e83b5d6..9ecc811 100644 --- a/server/index.ts +++ b/server/index.ts @@ -9,7 +9,7 @@ import * as WebSocket from "ws"; import { ServerState } from "./state"; import { createApp } from "./express"; -import { WebSocketApi } from "./websocket"; +import { WebSocketApi } from "./sprinklersRpc/websocketServer"; const state = new ServerState(); const app = createApp(state); diff --git a/server/websocket/index.ts b/server/sprinklersRpc/websocketServer.ts similarity index 97% rename from server/websocket/index.ts rename to server/sprinklersRpc/websocketServer.ts index dc40e15..7eaba5a 100644 --- a/server/websocket/index.ts +++ b/server/sprinklersRpc/websocketServer.ts @@ -4,10 +4,10 @@ import * as WebSocket from "ws"; import * as rpc from "@common/jsonRpc"; import log from "@common/logger"; -import * as deviceRequests from "@common/sprinklers/deviceRequests"; -import { ErrorCode } from "@common/sprinklers/ErrorCode"; -import * as schema from "@common/sprinklers/schema"; -import * as ws from "@common/sprinklers/websocketData"; +import * as deviceRequests from "@common/sprinklersRpc/deviceRequests"; +import { ErrorCode } from "@common/sprinklersRpc/ErrorCode"; +import * as schema from "@common/sprinklersRpc/schema"; +import * as ws from "@common/sprinklersRpc/websocketData"; import { TokenClaims, verifyToken } from "../express/authentication"; import { ServerState } from "../state"; diff --git a/server/state.ts b/server/state.ts index ea3be22..6b5b175 100644 --- a/server/state.ts +++ b/server/state.ts @@ -1,5 +1,5 @@ import logger from "@common/logger"; -import * as mqtt from "@common/sprinklers/mqtt"; +import * as mqtt from "@common/sprinklersRpc/mqtt"; import { Database } from "./models/Database"; export class ServerState { diff --git a/yarn.lock b/yarn.lock index 0835524..0691405 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4138,6 +4138,10 @@ mobx-react@^5.2.3: hoist-non-react-statics "^2.5.0" react-lifecycles-compat "^3.0.2" +mobx-utils@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-5.0.0.tgz#384e805064c237b9a9446788a9e68278a3437610" + mobx@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.0.3.tgz#53b97f2a0f9b0dd7774c96249f81bf2d513d8e1c"