mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-29 00:17:48 +01:00
nice error handling
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { IAppState } from "~redux/reducers";
|
||||
interface ILoginComponentProps {
|
||||
inProgress: boolean;
|
||||
error: string;
|
||||
spinner: boolean;
|
||||
login: (username: string, password: string) => void;
|
||||
}
|
||||
|
||||
@@ -31,8 +32,10 @@ export class LoginComponent extends React.PureComponent<
|
||||
|
||||
public submit() {
|
||||
const { username, password } = this.state;
|
||||
if (!this.props.inProgress) {
|
||||
this.props.login(username, password);
|
||||
}
|
||||
}
|
||||
|
||||
public updateFields(e: React.FormEvent<HTMLInputElement>) {
|
||||
const { value, name } = e.currentTarget;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
17
src/app.ts
17
src/app.ts
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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() };
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user