diff --git a/frontend/src/Auth/Auth.scss b/frontend/src/Auth/Auth.scss index 7e90f35..768a432 100644 --- a/frontend/src/Auth/Auth.scss +++ b/frontend/src/Auth/Auth.scss @@ -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; } } } diff --git a/frontend/src/Auth/Login.tsx b/frontend/src/Auth/Login.tsx index df22373..b38a25d 100644 --- a/frontend/src/Auth/Login.tsx +++ b/frontend/src/Auth/Login.tsx @@ -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) { @@ -63,12 +66,13 @@ export class LoginComponent extends React.PureComponent< />
-
{this.props.error}
+
{this.props.error}
@@ -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) { diff --git a/frontend/src/redux/auth/actions.ts b/frontend/src/redux/auth/actions.ts index aeb31fd..d0d4d22 100644 --- a/frontend/src/redux/auth/actions.ts +++ b/frontend/src/redux/auth/actions.ts @@ -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; diff --git a/frontend/src/redux/auth/reducer.ts b/frontend/src/redux/auth/reducer.ts index 452f7e9..ac09931 100644 --- a/frontend/src/redux/auth/reducer.ts +++ b/frontend/src/redux/auth/reducer.ts @@ -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 = ( @@ -20,14 +22,19 @@ export const auth: Reducer = ( ) => { 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; diff --git a/frontend/src/redux/auth/sagas.ts b/frontend/src/redux/auth/sagas.ts index 59296a5..c2cd516 100644 --- a/frontend/src/redux/auth/sagas.ts +++ b/frontend/src/redux/auth/sagas.ts @@ -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; diff --git a/src/app.ts b/src/app.ts index 8ea7086..071568b 100644 --- a/src/app.ts +++ b/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, + }; +}); diff --git a/src/routes/users.ts b/src/routes/users.ts index 1cd25d8..6fb5802 100644 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -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() }; }); diff --git a/tests/integration/users.test.ts b/tests/integration/users.test.ts index 6bc1ce2..00da75a 100644 --- a/tests/integration/users.test.ts +++ b/tests/integration/users.test.ts @@ -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; }); });