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 {
|
.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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.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);
|
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() };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user