Alex Mikhalev
7 years ago
39 changed files with 294 additions and 93 deletions
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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<any> { |
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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(); |
|
||||||
} |
|
||||||
} |
|
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -1,3 +1,9 @@ |
|||||||
export { UiMessage, UiStore } from "./UiStore"; |
export { UiMessage, UiStore } from "./UiStore"; |
||||||
export * from "./reactContext"; |
export * from "./reactContext"; |
||||||
export { default as StateBase } from "./StateBase"; |
export { ClientState as StateBase } from "./ClientState"; |
||||||
|
|
||||||
|
import ClientState from "./ClientState"; |
||||||
|
|
||||||
|
|
||||||
|
export class WebApiState extends ClientState { |
||||||
|
} |
||||||
|
@ -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}`); |
|
||||||
} |
|
@ -0,0 +1,7 @@ |
|||||||
|
export default interface TokenClaims { |
||||||
|
iss: string; |
||||||
|
type: "access" | "refresh"; |
||||||
|
aud: string; |
||||||
|
name: string; |
||||||
|
exp: number; |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -1,7 +1,7 @@ |
|||||||
import { ConnectionState } from "./ConnectionState"; |
import { ConnectionState } from "./ConnectionState"; |
||||||
import { SprinklersDevice } from "./SprinklersDevice"; |
import { SprinklersDevice } from "./SprinklersDevice"; |
||||||
|
|
||||||
export interface ISprinklersApi { |
export interface SprinklersRPC { |
||||||
readonly connectionState: ConnectionState; |
readonly connectionState: ConnectionState; |
||||||
readonly connected: boolean; |
readonly connected: boolean; |
||||||
|
|
@ -1,5 +1,5 @@ |
|||||||
// export * from "./Duration";
|
// export * from "./Duration";
|
||||||
export * from "./ISprinklersApi"; |
export * from "./SprinklersRPC"; |
||||||
export * from "./Program"; |
export * from "./Program"; |
||||||
export * from "./schedule"; |
export * from "./schedule"; |
||||||
export * from "./Section"; |
export * from "./Section"; |
@ -1,7 +1,7 @@ |
|||||||
import * as rpc from "../jsonRpc/index"; |
import * as rpc from "../jsonRpc/index"; |
||||||
|
|
||||||
import { Response as ResponseData } from "@common/sprinklers/deviceRequests"; |
import { Response as ResponseData } from "@common/sprinklersRpc/deviceRequests"; |
||||||
import { ErrorCode } from "@common/sprinklers/ErrorCode"; |
import { ErrorCode } from "@common/sprinklersRpc/ErrorCode"; |
||||||
|
|
||||||
export interface IAuthenticateRequest { |
export interface IAuthenticateRequest { |
||||||
accessToken: string; |
accessToken: string; |
Loading…
Reference in new issue