Improved error handling, and error display in login form
This commit is contained in:
parent
4f0efd9551
commit
7ed3096b6f
@ -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 {
|
||||
|
||||
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 {
|
||||
})
|
||||
.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 }> {
|
||||
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 }> {
|
||||
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,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";
|
||||
|
@ -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 {
|
||||
...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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
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!,
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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
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 { authentication } from "./authentication";
|
||||
import errorHandler from "./errorHandler";
|
||||
|
||||
export function createApp(state: ServerState) {
|
||||
const app = express();
|
||||
@ -46,5 +47,7 @@ export function createApp(state: ServerState) {
|
||||
|
||||
serveApp(app);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -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…
x
Reference in New Issue
Block a user