Browse Source

Improved error handling, and error display in login form

update-deps
Alex Mikhalev 7 years ago
parent
commit
7ed3096b6f
  1. 13
      app/pages/LoginPage.tsx
  2. 2
      app/sprinklersRpc/websocketClient.ts
  3. 21
      app/state/HttpApi.ts
  4. 6
      app/state/TokenStore.ts
  5. 4
      common/ApiError.ts
  6. 0
      common/ErrorCode.ts
  7. 0
      common/httpApi/index.ts
  8. 2
      common/sprinklersRpc/websocketData.ts
  9. 7
      server/express/authentication.ts
  10. 14
      server/express/errorHandler.ts
  11. 3
      server/express/index.ts
  12. 2
      server/sprinklersRpc/websocketServer.ts

13
app/pages/LoginPage.tsx

@ -1,15 +1,17 @@ @@ -1,15 +1,17 @@
import { AppState, injectState } from "@app/state";
import log from "@common/logger";
import { computed, observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { Container, Dimmer, Form, Header, InputOnChangeData, Loader, Segment } from "semantic-ui-react";
import { Container, Dimmer, Form, Header, InputOnChangeData, Loader, Message, Segment } from "semantic-ui-react";
import { AppState, injectState } from "@app/state";
import log from "@common/logger";
class LoginPageState {
@observable username = "";
@observable password = "";
@observable loading: boolean = false;
@observable error: string | null = null;
@computed get canLogin() {
return this.username.length > 0 && this.password.length > 0;
@ -25,6 +27,7 @@ class LoginPageState { @@ -25,6 +27,7 @@ class LoginPageState {
login(appState: AppState) {
this.loading = true;
this.error = null;
appState.tokenStore.grantPassword(this.username, this.password)
.then(() => {
this.loading = false;
@ -33,6 +36,7 @@ class LoginPageState { @@ -33,6 +36,7 @@ class LoginPageState {
})
.catch((err) => {
this.loading = false;
this.error = err.message;
log.error({ err }, "login error");
});
}
@ -42,7 +46,7 @@ class LoginPage extends React.Component<{ appState: AppState }> { @@ -42,7 +46,7 @@ class LoginPage extends React.Component<{ appState: AppState }> {
pageState = new LoginPageState();
render() {
const { username, password, canLogin, loading } = this.pageState;
const { username, password, canLogin, loading, error } = this.pageState;
return (
<Container className="loginPage">
<Segment>
@ -59,6 +63,7 @@ class LoginPage extends React.Component<{ appState: AppState }> { @@ -59,6 +63,7 @@ class LoginPage extends React.Component<{ appState: AppState }> {
type="password"
onChange={this.pageState.onPasswordChange}
/>
<Message error visible={error != null}>{error}</Message>
<Form.Button disabled={!canLogin} onClick={this.login}>Login</Form.Button>
</Form>
</Segment>

2
app/sprinklersRpc/websocketClient.ts

@ -2,10 +2,10 @@ import { action, observable, when } from "mobx"; @@ -2,10 +2,10 @@ import { action, observable, when } from "mobx";
import { update } from "serializr";
import { TokenStore } from "@app/state/TokenStore";
import { ErrorCode } from "@common/ErrorCode";
import * as rpc from "@common/jsonRpc";
import logger from "@common/logger";
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";

21
app/state/HttpApi.ts

@ -1,14 +1,8 @@ @@ -1,14 +1,8 @@
import { TokenStore } from "@app/state/TokenStore";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
export class HttpApiError extends Error {
name = "HttpApiError";
status: number;
constructor(message: string, status: number = 500) {
super(message);
this.status = status;
}
}
export { ApiError };
export default class HttpApi {
baseUrl: string;
@ -43,9 +37,14 @@ export default class HttpApi { @@ -43,9 +37,14 @@ export default class HttpApi {
...options,
};
const response = await fetch(this.baseUrl + url, options);
const responseBody = await response.json() || {};
let responseBody: any;
try {
responseBody = await response.json() || {};
} catch (e) {
throw new ApiError("Invalid JSON response", ErrorCode.Internal, e);
}
if (!response.ok) {
throw new HttpApiError(responseBody.message || response.statusText, response.status);
throw new ApiError(responseBody.message || response.statusText, responseBody.code, responseBody.data);
}
return responseBody;
}

6
app/state/TokenStore.ts

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
import { observable } from "mobx";
import HttpApi, { HttpApiError } from "@app/state/HttpApi";
import HttpApi, { ApiError } from "@app/state/HttpApi";
import { Token } from "@app/state/Token";
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/http";
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi";
import logger from "@common/logger";
const log = logger.child({ source: "TokenStore"});
@ -52,7 +52,7 @@ export class TokenStore { @@ -52,7 +52,7 @@ export class TokenStore {
async grantRefresh() {
if (!this.refreshToken.isValid) {
throw new HttpApiError("can not grant refresh with invalid refresh_token");
throw new ApiError("can not grant refresh with invalid refresh_token");
}
const request: TokenGrantRefreshRequest = {
grant_type: "refresh", refresh_token: this.refreshToken.token!,

4
server/express/errors.ts → common/ApiError.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { ErrorCode, toHttpStatus } from "@common/sprinklersRpc/ErrorCode";
import { ErrorCode, toHttpStatus } from "@common/ErrorCode";
export class ApiError extends Error {
export default class ApiError extends Error {
name = "ApiError";
statusCode: number;
code: ErrorCode;

0
common/sprinklersRpc/ErrorCode.ts → common/ErrorCode.ts

0
common/http.ts → common/httpApi/index.ts

2
common/sprinklersRpc/websocketData.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import * as rpc from "../jsonRpc/index";
import { ErrorCode } from "@common/ErrorCode";
import { Response as ResponseData } from "@common/sprinklersRpc/deviceRequests";
import { ErrorCode } from "@common/sprinklersRpc/ErrorCode";
export interface IAuthenticateRequest {
accessToken: string;

7
server/express/authentication.ts

@ -3,17 +3,16 @@ import Router from "express-promise-router"; @@ -3,17 +3,16 @@ import Router from "express-promise-router";
import * as jwt from "jsonwebtoken";
import TokenClaims from "@common/TokenClaims";
import {
TokenGrantPasswordRequest,
TokenGrantRefreshRequest,
TokenGrantRequest,
TokenGrantResponse,
} from "@common/http";
import { ErrorCode } from "@common/sprinklersRpc/ErrorCode";
} from "@common/httpApi";
import { ErrorCode } from "@common/ErrorCode";
import { User } from "../models/User";
import { ServerState } from "../state";
import { ApiError } from "./errors";
import ApiError from "@common/ApiError";
export { TokenClaims };

14
server/express/errorHandler.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
import * as express from "express";
import ApiError from "@common/ApiError";
const errorHandler: express.ErrorRequestHandler =
(err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err instanceof ApiError) {
res.status(err.statusCode).json(err.toJSON());
} else {
next(err);
}
};
export default errorHandler;

3
server/express/index.ts

@ -9,6 +9,7 @@ import serveApp from "./serveApp"; @@ -9,6 +9,7 @@ import serveApp from "./serveApp";
import { User } from "../models/User";
import { authentication } from "./authentication";
import errorHandler from "./errorHandler";
export function createApp(state: ServerState) {
const app = express();
@ -46,5 +47,7 @@ export function createApp(state: ServerState) { @@ -46,5 +47,7 @@ export function createApp(state: ServerState) {
serveApp(app);
app.use(errorHandler);
return app;
}

2
server/sprinklersRpc/websocketServer.ts

@ -5,7 +5,7 @@ import * as WebSocket from "ws"; @@ -5,7 +5,7 @@ import * as WebSocket from "ws";
import * as rpc from "@common/jsonRpc";
import log from "@common/logger";
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
import { ErrorCode } from "@common/sprinklersRpc/ErrorCode";
import { ErrorCode } from "@common/ErrorCode";
import * as schema from "@common/sprinklersRpc/schema";
import * as ws from "@common/sprinklersRpc/websocketData";
import { TokenClaims, verifyToken } from "../express/authentication";

Loading…
Cancel
Save