diff --git a/backend/src/app.ts b/backend/src/app.ts index 8920fc2..1bebc4b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -15,7 +15,7 @@ import { config, EnvType } from "~config"; import { userRouter } from "~routes/users"; import { devRouter } from "~routes/dev"; import { photosRouter } from "~routes/photos"; -import { TUserJWT } from "~shared/types"; +import { TAPIErrorResponse, TUserJWT } from "~shared/types"; export interface IAppState extends Koa.DefaultState { user?: TUserJWT; @@ -101,6 +101,6 @@ app.on("error", (err, ctx) => { console.log(err); ctx.body = { error: err.message, - data: false, - }; + data: null, + } as TAPIErrorResponse; }); diff --git a/backend/src/tests/integration/users.test.ts b/backend/src/tests/integration/users.test.ts index 3256893..3738c9e 100644 --- a/backend/src/tests/integration/users.test.ts +++ b/backend/src/tests/integration/users.test.ts @@ -83,7 +83,7 @@ describe("users", function () { const body = response.body as TUserLoginRespBody; expect(body.error).to.be.equal("User not found"); - expect(body.data).to.be.false; + expect(body.data).to.be.null; }); it("should signup user", async function () { @@ -127,7 +127,7 @@ describe("users", function () { const body = response.body as TUserSignupRespBody; expect(body.error).to.be.equal("Signups not allowed"); - expect(body.data).to.be.false; + expect(body.data).to.be.null; }); it("should signup first user and it should be admin, do not signup new users (by default)", async function () { @@ -170,7 +170,7 @@ describe("users", function () { const body2 = response2.body as TUserSignupRespBody; expect(body2.error).to.be.equal("Signups not allowed"); - expect(body2.data).to.be.false; + expect(body2.data).to.be.null; }); it("should signup first user and it should be admin, but not new ones", async function () { @@ -240,7 +240,7 @@ describe("users", function () { const body = response.body as TUserSignupRespBody; expect(body.error).to.be.equal("User already exists"); - expect(body.data).to.be.false; + expect(body.data).to.be.null; }); it("should change user's password", async function () { @@ -291,6 +291,6 @@ describe("users", function () { const badLoginBody = badLoginResponse.body as TUserLoginRespBody; expect(badLoginBody.error).to.be.equal("User not found"); - expect(badLoginBody.data).to.be.false; + expect(badLoginBody.data).to.be.null; }); }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2013e3a..c4f7c54 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "@parcel/config-default": "^2.9.3", "@parcel/transformer-sass": "^2.9.3", "@parcel/transformer-typescript-tsc": "^2.9.3", + "@reduxjs/toolkit": "^1.9.5", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", "@wojtekmaj/enzyme-adapter-react-17": "^0", @@ -2989,6 +2990,29 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -6874,6 +6898,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -10097,6 +10130,14 @@ "@redux-saga/core": "^1.2.3" } }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -10136,6 +10177,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", @@ -13385,6 +13431,17 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -16126,6 +16183,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -18387,6 +18449,12 @@ "@redux-saga/core": "^1.2.3" } }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} + }, "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -18417,6 +18485,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index b69a7ef..1216046 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@parcel/config-default": "^2.9.3", "@parcel/transformer-sass": "^2.9.3", "@parcel/transformer-typescript-tsc": "^2.9.3", + "@reduxjs/toolkit": "^1.9.5", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", "@wojtekmaj/enzyme-adapter-react-17": "^0", diff --git a/frontend/src/redux/auth/sagas.ts b/frontend/src/redux/auth/sagas.ts index 56da806..e3b1a29 100644 --- a/frontend/src/redux/auth/sagas.ts +++ b/frontend/src/redux/auth/sagas.ts @@ -26,9 +26,8 @@ function* startSpinner() { function* authStart(action: IAuthStartAction) { const { username, password } = action.payload; + const spinner = yield fork(startSpinner); try { - const spinner = yield fork(startSpinner); - const { response, timeout } = yield race({ response: call(login, username, password), timeout: delay(10000), @@ -47,15 +46,15 @@ function* authStart(action: IAuthStartAction) { yield put(authFail(response.error)); } } catch (e) { + yield cancel(spinner); yield put(authFail("Internal error")); } } function* signupStart(action: ISignupStartAction) { const { username, password, email } = action.payload; + const spinner = yield fork(startSpinner); try { - const spinner = yield fork(startSpinner); - const { response, timeout } = yield race({ response: call(signup, username, password, email), timeout: delay(10000), @@ -74,6 +73,7 @@ function* signupStart(action: ISignupStartAction) { yield put(authFail(response.error)); } } catch (e) { + yield cancel(spinner); yield put(authFail(e.toString())); } } diff --git a/frontend/src/redux/store.ts b/frontend/src/redux/store.ts index cb35c00..7df3721 100644 --- a/frontend/src/redux/store.ts +++ b/frontend/src/redux/store.ts @@ -1,21 +1,21 @@ -import { applyMiddleware, createStore } from "redux"; import { composeWithDevTools } from "redux-devtools-extension"; import { persistStore } from "redux-persist"; import createSagaMiddlware from "redux-saga"; -import { rootReducer } from "../redux/reducers"; +import { configureStore } from "@reduxjs/toolkit"; import { setToken } from "./api/utils"; import { authSaga } from "./auth/sagas"; import { photosSaga } from "./photos/sagas"; import { getUser } from "./user/actions"; import { userSaga } from "./user/sagas"; +import { rootReducer } from "../redux/reducers"; const sagaMiddleware = createSagaMiddlware(); -export const store = createStore( - rootReducer, - composeWithDevTools(applyMiddleware(sagaMiddleware)), -); +export const store = configureStore({ + reducer: rootReducer, + middleware: [sagaMiddleware], +}); export const persistor = persistStore(store, null, () => { const state = store.getState();