Improved error handling, and error display in login form
This commit is contained in:
parent
4f0efd9551
commit
7ed3096b6f
app
common
server
@ -1,15 +1,17 @@
|
|||||||
import { AppState, injectState } from "@app/state";
|
|
||||||
import log from "@common/logger";
|
|
||||||
import { computed, observable } from "mobx";
|
import { computed, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "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 {
|
class LoginPageState {
|
||||||
@observable username = "";
|
@observable username = "";
|
||||||
@observable password = "";
|
@observable password = "";
|
||||||
|
|
||||||
@observable loading: boolean = false;
|
@observable loading: boolean = false;
|
||||||
|
@observable error: string | null = null;
|
||||||
|
|
||||||
@computed get canLogin() {
|
@computed get canLogin() {
|
||||||
return this.username.length > 0 && this.password.length > 0;
|
return this.username.length > 0 && this.password.length > 0;
|
||||||
@ -25,6 +27,7 @@ class LoginPageState {
|
|||||||
|
|
||||||
login(appState: AppState) {
|
login(appState: AppState) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
appState.tokenStore.grantPassword(this.username, this.password)
|
appState.tokenStore.grantPassword(this.username, this.password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@ -33,6 +36,7 @@ class LoginPageState {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.error = err.message;
|
||||||
log.error({ err }, "login error");
|
log.error({ err }, "login error");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -42,7 +46,7 @@ class LoginPage extends React.Component<{ appState: AppState }> {
|
|||||||
pageState = new LoginPageState();
|
pageState = new LoginPageState();
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { username, password, canLogin, loading } = this.pageState;
|
const { username, password, canLogin, loading, error } = this.pageState;
|
||||||
return (
|
return (
|
||||||
<Container className="loginPage">
|
<Container className="loginPage">
|
||||||
<Segment>
|
<Segment>
|
||||||
@ -59,6 +63,7 @@ class LoginPage extends React.Component<{ appState: AppState }> {
|
|||||||
type="password"
|
type="password"
|
||||||
onChange={this.pageState.onPasswordChange}
|
onChange={this.pageState.onPasswordChange}
|
||||||
/>
|
/>
|
||||||
|
<Message error visible={error != null}>{error}</Message>
|
||||||
<Form.Button disabled={!canLogin} onClick={this.login}>Login</Form.Button>
|
<Form.Button disabled={!canLogin} onClick={this.login}>Login</Form.Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
@ -2,10 +2,10 @@ import { action, observable, when } from "mobx";
|
|||||||
import { update } from "serializr";
|
import { update } from "serializr";
|
||||||
|
|
||||||
import { TokenStore } from "@app/state/TokenStore";
|
import { TokenStore } from "@app/state/TokenStore";
|
||||||
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import * as rpc from "@common/jsonRpc";
|
import * as rpc from "@common/jsonRpc";
|
||||||
import logger from "@common/logger";
|
import logger from "@common/logger";
|
||||||
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
||||||
import { ErrorCode } from "@common/sprinklersRpc/ErrorCode";
|
|
||||||
import * as s from "@common/sprinklersRpc/index";
|
import * as s from "@common/sprinklersRpc/index";
|
||||||
import * as schema from "@common/sprinklersRpc/schema/index";
|
import * as schema from "@common/sprinklersRpc/schema/index";
|
||||||
import { seralizeRequest } from "@common/sprinklersRpc/schema/requests";
|
import { seralizeRequest } from "@common/sprinklersRpc/schema/requests";
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
import { TokenStore } from "@app/state/TokenStore";
|
import { TokenStore } from "@app/state/TokenStore";
|
||||||
|
import ApiError from "@common/ApiError";
|
||||||
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
|
|
||||||
export class HttpApiError extends Error {
|
export { ApiError };
|
||||||
name = "HttpApiError";
|
|
||||||
status: number;
|
|
||||||
|
|
||||||
constructor(message: string, status: number = 500) {
|
|
||||||
super(message);
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class HttpApi {
|
export default class HttpApi {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@ -43,9 +37,14 @@ export default class HttpApi {
|
|||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
const response = await fetch(this.baseUrl + url, 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) {
|
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;
|
return responseBody;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { observable } from "mobx";
|
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 { Token } from "@app/state/Token";
|
||||||
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/http";
|
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi";
|
||||||
import logger from "@common/logger";
|
import logger from "@common/logger";
|
||||||
|
|
||||||
const log = logger.child({ source: "TokenStore"});
|
const log = logger.child({ source: "TokenStore"});
|
||||||
@ -52,7 +52,7 @@ export class TokenStore {
|
|||||||
|
|
||||||
async grantRefresh() {
|
async grantRefresh() {
|
||||||
if (!this.refreshToken.isValid) {
|
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 = {
|
const request: TokenGrantRefreshRequest = {
|
||||||
grant_type: "refresh", refresh_token: this.refreshToken.token!,
|
grant_type: "refresh", refresh_token: this.refreshToken.token!,
|
||||||
|
@ -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";
|
name = "ApiError";
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
code: ErrorCode;
|
code: ErrorCode;
|
@ -1,7 +1,7 @@
|
|||||||
import * as rpc from "../jsonRpc/index";
|
import * as rpc from "../jsonRpc/index";
|
||||||
|
|
||||||
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import { Response as ResponseData } from "@common/sprinklersRpc/deviceRequests";
|
import { Response as ResponseData } from "@common/sprinklersRpc/deviceRequests";
|
||||||
import { ErrorCode } from "@common/sprinklersRpc/ErrorCode";
|
|
||||||
|
|
||||||
export interface IAuthenticateRequest {
|
export interface IAuthenticateRequest {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
@ -3,17 +3,16 @@ import Router from "express-promise-router";
|
|||||||
import * as jwt from "jsonwebtoken";
|
import * as jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import TokenClaims from "@common/TokenClaims";
|
import TokenClaims from "@common/TokenClaims";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TokenGrantPasswordRequest,
|
TokenGrantPasswordRequest,
|
||||||
TokenGrantRefreshRequest,
|
TokenGrantRefreshRequest,
|
||||||
TokenGrantRequest,
|
TokenGrantRequest,
|
||||||
TokenGrantResponse,
|
TokenGrantResponse,
|
||||||
} from "@common/http";
|
} from "@common/httpApi";
|
||||||
import { ErrorCode } from "@common/sprinklersRpc/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import { User } from "../models/User";
|
import { User } from "../models/User";
|
||||||
import { ServerState } from "../state";
|
import { ServerState } from "../state";
|
||||||
import { ApiError } from "./errors";
|
import ApiError from "@common/ApiError";
|
||||||
|
|
||||||
export { TokenClaims };
|
export { TokenClaims };
|
||||||
|
|
||||||
|
14
server/express/errorHandler.ts
Normal file
14
server/express/errorHandler.ts
Normal file
@ -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;
|
@ -9,6 +9,7 @@ import serveApp from "./serveApp";
|
|||||||
|
|
||||||
import { User } from "../models/User";
|
import { User } from "../models/User";
|
||||||
import { authentication } from "./authentication";
|
import { authentication } from "./authentication";
|
||||||
|
import errorHandler from "./errorHandler";
|
||||||
|
|
||||||
export function createApp(state: ServerState) {
|
export function createApp(state: ServerState) {
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -46,5 +47,7 @@ export function createApp(state: ServerState) {
|
|||||||
|
|
||||||
serveApp(app);
|
serveApp(app);
|
||||||
|
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import * as WebSocket from "ws";
|
|||||||
import * as rpc from "@common/jsonRpc";
|
import * as rpc from "@common/jsonRpc";
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
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 schema from "@common/sprinklersRpc/schema";
|
||||||
import * as ws from "@common/sprinklersRpc/websocketData";
|
import * as ws from "@common/sprinklersRpc/websocketData";
|
||||||
import { TokenClaims, verifyToken } from "../express/authentication";
|
import { TokenClaims, verifyToken } from "../express/authentication";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user