Alex Mikhalev
7 years ago
39 changed files with 294 additions and 93 deletions
@ -0,0 +1,26 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 { |
||||
} |
||||
|
@ -1,14 +0,0 @@
@@ -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 @@
@@ -0,0 +1,7 @@
|
||||
export default interface TokenClaims { |
||||
iss: string; |
||||
type: "access" | "refresh"; |
||||
aud: string; |
||||
name: string; |
||||
exp: number; |
||||
} |
@ -0,0 +1,17 @@
@@ -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 @@
@@ -1,7 +1,7 @@
|
||||
import { ConnectionState } from "./ConnectionState"; |
||||
import { SprinklersDevice } from "./SprinklersDevice"; |
||||
|
||||
export interface ISprinklersApi { |
||||
export interface SprinklersRPC { |
||||
readonly connectionState: ConnectionState; |
||||
readonly connected: boolean; |
||||
|
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
||||
// export * from "./Duration";
|
||||
export * from "./ISprinklersApi"; |
||||
export * from "./SprinklersRPC"; |
||||
export * from "./Program"; |
||||
export * from "./schedule"; |
||||
export * from "./Section"; |
@ -1,7 +1,7 @@
@@ -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; |
Loading…
Reference in new issue