nice error handling

This commit is contained in:
2019-01-02 22:58:48 +03:00
parent 859dec1d44
commit 733cbb4f26
8 changed files with 84 additions and 25 deletions

View File

@@ -1,3 +1,4 @@
@import "~@blueprintjs/core/lib/scss/variables";
.AuthForm {
margin: auto;
margin-top: 10rem;
@@ -11,10 +12,13 @@
.buttons {
display: flex;
flex-direction: row;
align-items: baseline;
#error {
height: 1rem;
color: $pt-intent-danger;
}
button.submit {
margin-left: auto;
justify-self: flex-end;
align-self: flex-end;
}
}
}

View File

@@ -10,6 +10,7 @@ import { IAppState } from "~redux/reducers";
interface ILoginComponentProps {
inProgress: boolean;
error: string;
spinner: boolean;
login: (username: string, password: string) => void;
}
@@ -31,7 +32,9 @@ export class LoginComponent extends React.PureComponent<
public submit() {
const { username, password } = this.state;
this.props.login(username, password);
if (!this.props.inProgress) {
this.props.login(username, password);
}
}
public updateFields(e: React.FormEvent<HTMLInputElement>) {
@@ -63,12 +66,13 @@ export class LoginComponent extends React.PureComponent<
/>
</FormGroup>
<div className="buttons">
<div id="errors">{this.props.error}</div>
<div id="error">{this.props.error}</div>
<Button
loading={this.props.spinner}
className="submit"
intent="primary"
onClick={this.submit}
active={!this.props.inProgress}
disabled={this.props.spinner}
>
Login
</Button>
@@ -81,7 +85,11 @@ export class LoginComponent extends React.PureComponent<
}
function mapStateToProps(state: IAppState) {
return { inProgress: state.auth.inProgress, error: state.auth.error };
return {
inProgress: state.auth.inProgress,
error: state.auth.formError,
spinner: state.auth.formSpinner,
};
}
function mapDispatchToProps(dispatch: Dispatch) {

View File

@@ -4,6 +4,7 @@ export enum AuthTypes {
AUTH_START = "AUTH_START",
AUTH_SUCCESS = "AUTH_SUCCESS",
AUTH_FAIL = "AUTH_FAIL",
AUTH_START_FORM_SPINNER = "AUTH_START_FORM_SPINNER",
}
export interface IAuthStartActionAction extends Action {
@@ -28,6 +29,14 @@ export interface IAuthFailureActionAction extends Action {
};
}
export interface IAuthStartFormSpinnerAction extends Action {
type: AuthTypes.AUTH_START_FORM_SPINNER;
}
export function startFormSpinner(): IAuthStartFormSpinnerAction {
return { type: AuthTypes.AUTH_START_FORM_SPINNER };
}
export function authStart(
username: string,
password: string,
@@ -46,4 +55,5 @@ export function authFail(error: string): IAuthFailureActionAction {
export type AuthAction =
| IAuthStartActionAction
| IAuthSuccessActionAction
| IAuthFailureActionAction;
| IAuthFailureActionAction
| IAuthStartFormSpinnerAction;

View File

@@ -5,13 +5,15 @@ import { AuthAction, AuthTypes } from "./actions";
export interface IAuthState {
jwt: string | null;
inProgress: boolean;
error: string | null;
formError: string | null;
formSpinner: boolean;
}
const defaultAuthState: IAuthState = {
jwt: null,
inProgress: false,
error: null,
formError: null,
formSpinner: false,
};
export const auth: Reducer<IAuthState, AuthAction> = (
@@ -20,14 +22,19 @@ export const auth: Reducer<IAuthState, AuthAction> = (
) => {
switch (action.type) {
case AuthTypes.AUTH_START:
return { ...state, inProgress: true };
return { ...defaultAuthState, inProgress: true };
break;
case AuthTypes.AUTH_SUCCESS:
return { ...state, jwt: action.payload.jwt, inProgress: false };
return {
...defaultAuthState,
jwt: action.payload.jwt,
};
break;
case AuthTypes.AUTH_FAIL:
return { ...defaultAuthState, error: action.payload.error };
return { ...defaultAuthState, formError: action.payload.error };
break;
case AuthTypes.AUTH_START_FORM_SPINNER:
return { ...state, formSpinner: true };
default:
return state;
break;

View File

@@ -1,5 +1,5 @@
import { delay } from "redux-saga";
import { call, put, race, takeLatest } from "redux-saga/effects";
import { call, cancel, fork, put, race, takeLatest } from "redux-saga/effects";
import { login } from "~redux/api/auth";
import { setToken } from "~redux/api/utils";
@@ -8,16 +8,26 @@ import {
authSuccess,
AuthTypes,
IAuthStartActionAction,
startFormSpinner,
} from "./actions";
function* startSpinner() {
yield delay(300);
yield put(startFormSpinner());
}
function* authStart(action: IAuthStartActionAction) {
const { username, password } = action.payload;
try {
const spinner = yield fork(startSpinner);
const { response, timeout } = yield race({
response: call(login, username, password),
timeout: call(delay, 1000),
timeout: call(delay, 10000),
});
yield cancel(spinner);
if (timeout) {
yield put(authFail("Timeout"));
return;

View File

@@ -20,4 +20,21 @@ app.use(
}),
);
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = err.message;
ctx.app.emit("error", err, ctx);
}
});
app.use(userRouter.routes()).use(userRouter.allowedMethods());
app.on("error", (err, ctx) => {
ctx.body = {
error: err.message,
data: false,
};
});

View File

@@ -12,7 +12,7 @@ userRouter.get("/users/user", async ctx => {
const user = await User.findOne(jwt.id);
ctx.body = { errors: false, data: user.toAuthJSON() };
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/login", async ctx => {
@@ -32,10 +32,10 @@ userRouter.post("/users/login", async ctx => {
const user = await User.findOne({ username });
if (!user || !(await user.verifyPassword(password))) {
ctx.throw(404);
ctx.throw(404, "User not found");
}
ctx.body = { errors: false, data: user.toAuthJSON() };
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/signup", async ctx => {
@@ -60,9 +60,10 @@ userRouter.post("/users/signup", async ctx => {
try {
await user.save();
} catch (e) {
// TODO: Errors
ctx.throw(400);
if (e.code === "ER_DUP_ENTRY") {
ctx.throw(400, "User already exists");
}
}
ctx.body = { errors: false, data: user.toAuthJSON() };
ctx.body = { error: false, data: user.toAuthJSON() };
});

View File

@@ -34,7 +34,7 @@ describe("users", () => {
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.errors).to.be.false;
expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
@@ -49,7 +49,7 @@ describe("users", () => {
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.errors).to.be.false;
expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
@@ -63,7 +63,8 @@ describe("users", () => {
.send({ username: "User1", password: "asdf" })
.expect(404);
expect(response.body).to.deep.equal({});
expect(response.body.error).to.be.equal("User not found");
expect(response.body.data).to.be.false;
});
it("should signup user", async () => {
@@ -74,7 +75,7 @@ describe("users", () => {
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.errors).to.be.false;
expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
@@ -90,6 +91,7 @@ describe("users", () => {
.send({ username: "User1", password: "NUser1" })
.expect(400);
expect(response.body).to.deep.equal({});
expect(response.body.error).to.be.equal("User already exists");
expect(response.body.data).to.be.false;
});
});