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 { .AuthForm {
margin: auto; margin: auto;
margin-top: 10rem; margin-top: 10rem;
@@ -11,10 +12,13 @@
.buttons { .buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline;
#error {
height: 1rem;
color: $pt-intent-danger;
}
button.submit { button.submit {
margin-left: auto; margin-left: auto;
justify-self: flex-end;
align-self: flex-end;
} }
} }
} }

View File

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

View File

@@ -4,6 +4,7 @@ export enum AuthTypes {
AUTH_START = "AUTH_START", AUTH_START = "AUTH_START",
AUTH_SUCCESS = "AUTH_SUCCESS", AUTH_SUCCESS = "AUTH_SUCCESS",
AUTH_FAIL = "AUTH_FAIL", AUTH_FAIL = "AUTH_FAIL",
AUTH_START_FORM_SPINNER = "AUTH_START_FORM_SPINNER",
} }
export interface IAuthStartActionAction extends Action { 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( export function authStart(
username: string, username: string,
password: string, password: string,
@@ -46,4 +55,5 @@ export function authFail(error: string): IAuthFailureActionAction {
export type AuthAction = export type AuthAction =
| IAuthStartActionAction | IAuthStartActionAction
| IAuthSuccessActionAction | IAuthSuccessActionAction
| IAuthFailureActionAction; | IAuthFailureActionAction
| IAuthStartFormSpinnerAction;

View File

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

View File

@@ -1,5 +1,5 @@
import { delay } from "redux-saga"; 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 { login } from "~redux/api/auth";
import { setToken } from "~redux/api/utils"; import { setToken } from "~redux/api/utils";
@@ -8,16 +8,26 @@ import {
authSuccess, authSuccess,
AuthTypes, AuthTypes,
IAuthStartActionAction, IAuthStartActionAction,
startFormSpinner,
} from "./actions"; } from "./actions";
function* startSpinner() {
yield delay(300);
yield put(startFormSpinner());
}
function* authStart(action: IAuthStartActionAction) { function* authStart(action: IAuthStartActionAction) {
const { username, password } = action.payload; const { username, password } = action.payload;
try { try {
const spinner = yield fork(startSpinner);
const { response, timeout } = yield race({ const { response, timeout } = yield race({
response: call(login, username, password), response: call(login, username, password),
timeout: call(delay, 1000), timeout: call(delay, 10000),
}); });
yield cancel(spinner);
if (timeout) { if (timeout) {
yield put(authFail("Timeout")); yield put(authFail("Timeout"));
return; 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.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); 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 => { userRouter.post("/users/login", async ctx => {
@@ -32,10 +32,10 @@ userRouter.post("/users/login", async ctx => {
const user = await User.findOne({ username }); const user = await User.findOne({ username });
if (!user || !(await user.verifyPassword(password))) { 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 => { userRouter.post("/users/signup", async ctx => {
@@ -60,9 +60,10 @@ userRouter.post("/users/signup", async ctx => {
try { try {
await user.save(); await user.save();
} catch (e) { } catch (e) {
// TODO: Errors if (e.code === "ER_DUP_ENTRY") {
ctx.throw(400); 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("Content-Type", /json/)
.expect(200); .expect(200);
expect(response.body.errors).to.be.false; expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON; const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
@@ -49,7 +49,7 @@ describe("users", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
expect(response.body.errors).to.be.false; expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON; const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
@@ -63,7 +63,8 @@ describe("users", () => {
.send({ username: "User1", password: "asdf" }) .send({ username: "User1", password: "asdf" })
.expect(404); .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 () => { it("should signup user", async () => {
@@ -74,7 +75,7 @@ describe("users", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
expect(response.body.errors).to.be.false; expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON; const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
@@ -90,6 +91,7 @@ describe("users", () => {
.send({ username: "User1", password: "NUser1" }) .send({ username: "User1", password: "NUser1" })
.expect(400); .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;
}); });
}); });