From 0ff391491d5528daa5fe181d0483504be189fcc4 Mon Sep 17 00:00:00 2001 From: seavor Date: Wed, 15 Apr 2026 21:48:03 -0500 Subject: [PATCH] refactor redux data model --- webclient/eslint.boundaries.mjs | 55 ++ webclient/eslint.config.mjs | 4 + webclient/package-lock.json | 740 +++++++++++++++- webclient/package.json | 4 +- webclient/src/api/AdminService.spec.ts | 46 - webclient/src/api/AdminService.tsx | 19 - .../src/api/AuthenticationService.spec.ts | 166 ---- webclient/src/api/AuthenticationService.tsx | 50 -- webclient/src/api/ModeratorService.spec.ts | 73 -- webclient/src/api/ModeratorService.tsx | 29 - webclient/src/api/RoomsService.spec.ts | 35 - webclient/src/api/RoomsService.tsx | 15 - webclient/src/api/SessionService.spec.ts | 100 --- webclient/src/api/SessionService.tsx | 43 - webclient/src/api/index.ts | 37 +- webclient/src/api/request/AdminRequestImpl.ts | 20 + .../api/request/AuthenticationRequestImpl.ts | 37 + .../src/api/request/ModeratorRequestImpl.ts | 37 + webclient/src/api/request/RoomsRequestImpl.ts | 16 + .../src/api/request/SessionRequestImpl.ts | 44 + webclient/src/api/request/index.ts | 23 + .../src/api/response/AdminResponseImpl.ts | 20 + .../src/api/response/GameResponseImpl.ts | 125 +++ .../src/api/response/ModeratorResponseImpl.ts | 45 + .../src/api/response/RoomResponseImpl.ts | 49 ++ .../src/api/response/SessionResponseImpl.ts | 232 +++++ webclient/src/api/response/index.ts | 23 + webclient/src/components/Guard/AuthGuard.tsx | 8 +- webclient/src/components/Guard/ModGuard.tsx | 8 +- .../src/components/KnownHosts/KnownHosts.tsx | 4 +- .../components/UserDisplay/UserDisplay.tsx | 14 +- webclient/src/containers/Account/Account.tsx | 18 +- webclient/src/containers/Layout/LeftNav.tsx | 28 +- webclient/src/containers/Login/Login.tsx | 22 +- webclient/src/containers/Logs/LogResults.tsx | 5 +- webclient/src/containers/Logs/Logs.tsx | 33 +- webclient/src/containers/Room/Games.tsx | 64 +- webclient/src/containers/Room/OpenGames.tsx | 64 +- webclient/src/containers/Room/Room.tsx | 8 +- webclient/src/containers/Server/Rooms.tsx | 38 +- webclient/src/containers/Server/Server.tsx | 4 +- .../src/forms/RegisterForm/RegisterForm.tsx | 6 +- webclient/src/hooks/useDebounce.ts | 25 +- webclient/src/hooks/useReduxEffect.tsx | 5 +- webclient/src/index.tsx | 37 +- webclient/src/store/common/SortUtil.ts | 39 +- .../src/store/common/normalizers.spec.ts | 30 +- webclient/src/store/common/normalizers.ts | 36 +- .../src/store/game/__mocks__/fixtures.ts | 49 +- webclient/src/store/game/game.actions.spec.ts | 107 ++- webclient/src/store/game/game.actions.ts | 212 +---- .../src/store/game/game.dispatch.spec.ts | 65 +- webclient/src/store/game/game.dispatch.ts | 64 +- webclient/src/store/game/game.interfaces.ts | 59 +- webclient/src/store/game/game.reducer.spec.ts | 515 ++++++----- webclient/src/store/game/game.reducer.ts | 824 ++++++------------ .../src/store/game/game.selectors.spec.ts | 4 +- webclient/src/store/game/game.selectors.ts | 42 +- webclient/src/store/game/game.types.ts | 70 +- webclient/src/store/index.ts | 2 - .../store/rooms/__mocks__/rooms-fixtures.ts | 69 +- .../src/store/rooms/rooms.actions.spec.ts | 39 +- webclient/src/store/rooms/rooms.actions.tsx | 74 +- .../src/store/rooms/rooms.dispatch.spec.ts | 24 +- webclient/src/store/rooms/rooms.dispatch.tsx | 23 +- .../src/store/rooms/rooms.interfaces.tsx | 7 - .../src/store/rooms/rooms.reducer.spec.ts | 181 ++-- webclient/src/store/rooms/rooms.reducer.tsx | 411 +++------ .../src/store/rooms/rooms.selectors.spec.ts | 51 +- webclient/src/store/rooms/rooms.selectors.tsx | 73 +- webclient/src/store/rooms/rooms.types.tsx | 30 +- .../store/server/__mocks__/server-fixtures.ts | 20 +- .../src/store/server/server.actions.spec.ts | 181 ++-- webclient/src/store/server/server.actions.ts | 244 +----- .../src/store/server/server.dispatch.spec.ts | 102 +-- webclient/src/store/server/server.dispatch.ts | 106 ++- .../src/store/server/server.interfaces.ts | 12 +- .../src/store/server/server.reducer.spec.ts | 386 ++++---- webclient/src/store/server/server.reducer.ts | 670 ++++++-------- .../src/store/server/server.selectors.spec.ts | 54 +- .../src/store/server/server.selectors.ts | 80 +- webclient/src/store/server/server.types.ts | 142 +-- webclient/src/types/enriched.ts | 102 ++- webclient/src/types/sort.ts | 3 +- webclient/src/websocket/WebClient.spec.ts | 95 +- webclient/src/websocket/WebClient.ts | 48 +- .../__mocks__/sessionCommandMocks.ts | 22 +- .../src/websocket/commands/admin/adjustMod.ts | 8 +- .../commands/admin/adminCommands.spec.ts | 51 +- .../websocket/commands/admin/reloadConfig.ts | 8 +- .../commands/admin/shutdownServer.ts | 18 +- .../commands/admin/updateServerMessage.ts | 8 +- .../src/websocket/commands/game/attachCard.ts | 4 +- .../commands/game/changeZoneProperties.ts | 4 +- .../src/websocket/commands/game/concede.ts | 4 +- .../websocket/commands/game/createArrow.ts | 4 +- .../websocket/commands/game/createCounter.ts | 4 +- .../websocket/commands/game/createToken.ts | 4 +- .../src/websocket/commands/game/deckSelect.ts | 4 +- .../src/websocket/commands/game/delCounter.ts | 4 +- .../websocket/commands/game/deleteArrow.ts | 4 +- .../src/websocket/commands/game/drawCards.ts | 4 +- .../src/websocket/commands/game/dumpZone.ts | 4 +- .../src/websocket/commands/game/flipCard.ts | 4 +- .../commands/game/gameCommands.spec.ts | 84 +- .../src/websocket/commands/game/gameSay.ts | 4 +- .../websocket/commands/game/incCardCounter.ts | 4 +- .../src/websocket/commands/game/incCounter.ts | 4 +- .../src/websocket/commands/game/judge.ts | 4 +- .../websocket/commands/game/kickFromGame.ts | 4 +- .../src/websocket/commands/game/leaveGame.ts | 4 +- .../src/websocket/commands/game/moveCard.ts | 4 +- .../src/websocket/commands/game/mulligan.ts | 4 +- .../src/websocket/commands/game/nextTurn.ts | 4 +- .../src/websocket/commands/game/readyStart.ts | 4 +- .../websocket/commands/game/revealCards.ts | 4 +- .../websocket/commands/game/reverseTurn.ts | 4 +- .../websocket/commands/game/setActivePhase.ts | 4 +- .../websocket/commands/game/setCardAttr.ts | 4 +- .../websocket/commands/game/setCardCounter.ts | 4 +- .../src/websocket/commands/game/setCounter.ts | 4 +- .../commands/game/setSideboardLock.ts | 8 +- .../commands/game/setSideboardPlan.ts | 8 +- .../src/websocket/commands/game/shuffle.ts | 4 +- .../src/websocket/commands/game/unconcede.ts | 4 +- .../src/websocket/commands/game/undoDraw.ts | 4 +- .../commands/moderator/banFromServer.ts | 7 +- .../commands/moderator/forceActivateUser.ts | 7 +- .../commands/moderator/getAdminNotes.ts | 7 +- .../commands/moderator/getBanHistory.ts | 7 +- .../commands/moderator/getWarnHistory.ts | 19 +- .../commands/moderator/getWarnList.ts | 7 +- .../commands/moderator/grantReplayAccess.ts | 7 +- .../moderator/moderatorCommands.spec.ts | 101 +-- .../commands/moderator/updateAdminNotes.ts | 7 +- .../commands/moderator/viewLogHistory.ts | 8 +- .../websocket/commands/moderator/warnUser.ts | 7 +- .../src/websocket/commands/room/createGame.ts | 7 +- .../src/websocket/commands/room/joinGame.ts | 7 +- .../src/websocket/commands/room/leaveRoom.ts | 7 +- .../commands/room/roomCommands.spec.ts | 51 +- .../src/websocket/commands/room/roomSay.ts | 4 +- .../websocket/commands/session/accountEdit.ts | 7 +- .../commands/session/accountImage.ts | 7 +- .../commands/session/accountPassword.ts | 7 +- .../websocket/commands/session/activate.ts | 10 +- .../websocket/commands/session/addToList.ts | 7 +- .../src/websocket/commands/session/connect.ts | 6 +- .../src/websocket/commands/session/deckDel.ts | 7 +- .../websocket/commands/session/deckDelDir.ts | 7 +- .../websocket/commands/session/deckList.ts | 7 +- .../websocket/commands/session/deckNewDir.ts | 7 +- .../websocket/commands/session/deckUpload.ts | 23 +- .../websocket/commands/session/disconnect.ts | 4 +- .../session/forgotPasswordChallenge.ts | 39 +- .../commands/session/forgotPasswordRequest.ts | 12 +- .../commands/session/forgotPasswordReset.ts | 31 +- .../commands/session/getGamesOfUser.ts | 7 +- .../websocket/commands/session/getUserInfo.ts | 7 +- .../websocket/commands/session/joinRoom.ts | 7 +- .../websocket/commands/session/listRooms.ts | 4 +- .../websocket/commands/session/listUsers.ts | 7 +- .../src/websocket/commands/session/login.ts | 18 +- .../src/websocket/commands/session/message.ts | 4 +- .../src/websocket/commands/session/ping.ts | 4 +- .../websocket/commands/session/register.ts | 27 +- .../commands/session/removeFromList.ts | 17 +- .../commands/session/replayDeleteMatch.ts | 17 +- .../commands/session/replayGetCode.ts | 4 +- .../websocket/commands/session/replayList.ts | 7 +- .../commands/session/replayModifyMatch.ts | 7 +- .../commands/session/replaySubmitCode.ts | 14 +- .../commands/session/requestPasswordSalt.ts | 12 +- .../session/sessionCommands-complex.spec.ts | 134 ++- .../session/sessionCommands-simple.spec.ts | 119 ++- .../commands/session/updateStatus.ts | 8 +- .../src/websocket/events/game/attachCard.ts | 4 +- .../events/game/changeZoneProperties.ts | 4 +- .../src/websocket/events/game/createArrow.ts | 4 +- .../websocket/events/game/createCounter.ts | 4 +- .../src/websocket/events/game/createToken.ts | 4 +- .../src/websocket/events/game/delCounter.ts | 4 +- .../src/websocket/events/game/deleteArrow.ts | 4 +- .../src/websocket/events/game/destroyCard.ts | 4 +- .../src/websocket/events/game/drawCards.ts | 4 +- .../src/websocket/events/game/dumpZone.ts | 4 +- .../src/websocket/events/game/flipCard.ts | 4 +- .../src/websocket/events/game/gameClosed.ts | 4 +- .../websocket/events/game/gameEvents.spec.ts | 186 ++-- .../websocket/events/game/gameHostChanged.ts | 4 +- .../src/websocket/events/game/gameSay.ts | 4 +- .../websocket/events/game/gameStateChanged.ts | 4 +- .../src/websocket/events/game/joinGame.ts | 5 +- webclient/src/websocket/events/game/kicked.ts | 4 +- .../src/websocket/events/game/leaveGame.ts | 4 +- .../src/websocket/events/game/moveCard.ts | 4 +- .../events/game/playerPropertiesChanged.ts | 4 +- .../src/websocket/events/game/revealCards.ts | 4 +- .../src/websocket/events/game/reverseTurn.ts | 4 +- .../src/websocket/events/game/rollDie.ts | 4 +- .../websocket/events/game/setActivePhase.ts | 4 +- .../websocket/events/game/setActivePlayer.ts | 4 +- .../src/websocket/events/game/setCardAttr.ts | 4 +- .../websocket/events/game/setCardCounter.ts | 4 +- .../src/websocket/events/game/setCounter.ts | 4 +- .../src/websocket/events/game/shuffle.ts | 4 +- .../src/websocket/events/room/joinRoom.ts | 4 +- .../src/websocket/events/room/leaveRoom.ts | 4 +- .../src/websocket/events/room/listGames.ts | 4 +- .../websocket/events/room/removeMessages.ts | 4 +- .../websocket/events/room/roomEvents.spec.ts | 42 +- .../src/websocket/events/room/roomSay.ts | 4 +- .../src/websocket/events/session/addToList.ts | 6 +- .../websocket/events/session/gameJoined.ts | 4 +- .../src/websocket/events/session/listRooms.ts | 4 +- .../websocket/events/session/notifyUser.ts | 4 +- .../events/session/removeFromList.ts | 6 +- .../websocket/events/session/replayAdded.ts | 4 +- .../events/session/serverCompleteList.ts | 6 +- .../events/session/serverIdentification.ts | 10 +- .../websocket/events/session/serverMessage.ts | 5 +- .../events/session/serverShutdown.ts | 4 +- .../events/session/sessionEvents.spec.ts | 141 +-- .../websocket/events/session/userJoined.ts | 4 +- .../src/websocket/events/session/userLeft.ts | 4 +- .../websocket/events/session/userMessage.ts | 4 +- webclient/src/websocket/index.ts | 3 +- .../websocket/interfaces/WebClientRequest.ts | 63 ++ .../websocket/interfaces/WebClientResponse.ts | 134 +++ webclient/src/websocket/interfaces/index.ts | 17 + .../persistence/AdminPersistence.spec.ts | 33 - .../websocket/persistence/AdminPersistence.ts | 19 - .../persistence/GamePersistence.spec.ts | 208 ----- .../websocket/persistence/GamePersistence.ts | 121 --- .../persistence/ModeratorPersistence.spec.ts | 72 -- .../persistence/ModeratorPersistence.ts | 44 - .../persistence/RoomPersistence.spec.ts | 94 -- .../websocket/persistence/RoomPersistence.ts | 48 - .../persistence/SessionPersistence.spec.ts | 401 --------- .../persistence/SessionPersistence.ts | 229 ----- webclient/src/websocket/persistence/index.ts | 5 - .../services/WebSocketService.spec.ts | 65 +- .../websocket/services/WebSocketService.ts | 21 +- 243 files changed, 5212 insertions(+), 5963 deletions(-) create mode 100644 webclient/eslint.boundaries.mjs delete mode 100644 webclient/src/api/AdminService.spec.ts delete mode 100644 webclient/src/api/AdminService.tsx delete mode 100644 webclient/src/api/AuthenticationService.spec.ts delete mode 100644 webclient/src/api/AuthenticationService.tsx delete mode 100644 webclient/src/api/ModeratorService.spec.ts delete mode 100644 webclient/src/api/ModeratorService.tsx delete mode 100644 webclient/src/api/RoomsService.spec.ts delete mode 100644 webclient/src/api/RoomsService.tsx delete mode 100644 webclient/src/api/SessionService.spec.ts delete mode 100644 webclient/src/api/SessionService.tsx create mode 100644 webclient/src/api/request/AdminRequestImpl.ts create mode 100644 webclient/src/api/request/AuthenticationRequestImpl.ts create mode 100644 webclient/src/api/request/ModeratorRequestImpl.ts create mode 100644 webclient/src/api/request/RoomsRequestImpl.ts create mode 100644 webclient/src/api/request/SessionRequestImpl.ts create mode 100644 webclient/src/api/request/index.ts create mode 100644 webclient/src/api/response/AdminResponseImpl.ts create mode 100644 webclient/src/api/response/GameResponseImpl.ts create mode 100644 webclient/src/api/response/ModeratorResponseImpl.ts create mode 100644 webclient/src/api/response/RoomResponseImpl.ts create mode 100644 webclient/src/api/response/SessionResponseImpl.ts create mode 100644 webclient/src/api/response/index.ts create mode 100644 webclient/src/websocket/interfaces/WebClientRequest.ts create mode 100644 webclient/src/websocket/interfaces/WebClientResponse.ts create mode 100644 webclient/src/websocket/interfaces/index.ts delete mode 100644 webclient/src/websocket/persistence/AdminPersistence.spec.ts delete mode 100644 webclient/src/websocket/persistence/AdminPersistence.ts delete mode 100644 webclient/src/websocket/persistence/GamePersistence.spec.ts delete mode 100644 webclient/src/websocket/persistence/GamePersistence.ts delete mode 100644 webclient/src/websocket/persistence/ModeratorPersistence.spec.ts delete mode 100644 webclient/src/websocket/persistence/ModeratorPersistence.ts delete mode 100644 webclient/src/websocket/persistence/RoomPersistence.spec.ts delete mode 100644 webclient/src/websocket/persistence/RoomPersistence.ts delete mode 100644 webclient/src/websocket/persistence/SessionPersistence.spec.ts delete mode 100644 webclient/src/websocket/persistence/SessionPersistence.ts delete mode 100644 webclient/src/websocket/persistence/index.ts diff --git a/webclient/eslint.boundaries.mjs b/webclient/eslint.boundaries.mjs new file mode 100644 index 000000000..0d67c6ae9 --- /dev/null +++ b/webclient/eslint.boundaries.mjs @@ -0,0 +1,55 @@ +import boundaries from 'eslint-plugin-boundaries'; + +const elements = [ + { type: 'api', pattern: ['src/api/**'] }, + { type: 'components', pattern: ['src/components/**'] }, + { type: 'containers', pattern: ['src/containers/**'] }, + { type: 'dialogs', pattern: ['src/dialogs/**'] }, + { type: 'forms', pattern: ['src/forms/**'] }, + { type: 'generated', pattern: ['src/generated/**'] }, + { type: 'hooks', pattern: ['src/hooks/**'] }, + { type: 'images', pattern: ['src/images/**'] }, + { type: 'services', pattern: ['src/services/**'] }, + { type: 'store', pattern: ['src/store/**'] }, + { type: 'types', pattern: ['src/types/**'] }, + { type: 'websocket', pattern: ['src/websocket/**'] }, +]; + +const types = (...types) => types.map((type) => ({ to: { type } })); + +const rules = [ + { from: { type: 'generated' }, allow: [] }, + { from: { type: 'types' }, allow: types('generated') }, + + { from: { type: 'websocket' }, allow: types('types') }, + { from: { type: 'store' }, allow: types('types') }, + { from: { type: 'api' }, allow: types('types', 'store', 'websocket') }, + + { from: { type: 'hooks' }, allow: types('services', 'types') }, + { from: { type: 'images' }, allow: types('types') }, + { from: { type: 'services' }, allow: types('api', 'store', 'types') }, + + { from: { type: 'components' }, allow: types('api', 'dialogs', 'forms', 'hooks', 'images', 'services', 'types', 'store') }, + { from: { type: 'containers' }, allow: types('api', 'components', 'dialogs', 'forms', 'hooks', 'images', 'services', 'types', 'store') }, + { from: { type: 'dialogs' }, allow: types('components', 'forms', 'hooks', 'services', 'types', 'store') }, + { from: { type: 'forms' }, allow: types('components', 'hooks', 'types', 'services', 'store') }, +]; + +export const boundariesConfig = { + plugins: { boundaries }, + settings: { + 'boundaries/elements': elements, + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + project: './tsconfig.json', + }, + }, + }, + rules: { + 'boundaries/dependencies': ['error', { + default: 'disallow', + rules, + }], + }, +}; diff --git a/webclient/eslint.config.mjs b/webclient/eslint.config.mjs index f5a46a5a7..28a00a0ae 100644 --- a/webclient/eslint.config.mjs +++ b/webclient/eslint.config.mjs @@ -1,6 +1,7 @@ import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import globals from 'globals'; +import { boundariesConfig } from './eslint.boundaries.mjs'; export default tseslint.config( // Global ignores @@ -12,6 +13,9 @@ export default tseslint.config( // TypeScript recommended (sets up parser + plugin) ...tseslint.configs.recommended, + // Enforce module boundaries + boundariesConfig, + // Project-specific config { languageOptions: { diff --git a/webclient/package-lock.json b/webclient/package-lock.json index 9f48a49b2..ed7f1854a 100644 --- a/webclient/package-lock.json +++ b/webclient/package-lock.json @@ -23,7 +23,6 @@ "i18next-browser-languagedetector": "^8.2.1", "i18next-icu": "^2.0.3", "intl-messageformat": "^11.2.1", - "lodash": "^4.17.21", "prop-types": "^15.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -45,7 +44,6 @@ "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^16.3.2", "@types/dompurify": "^3.0.5", - "@types/lodash": "^4.14.179", "@types/node": "^22.19.17", "@types/prop-types": "^15.7.4", "@types/react": "^19.0.0", @@ -57,6 +55,8 @@ "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.4", "eslint": "^10.2.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^6.0.2", "fs-extra": "^11.3.4", "globals": "^17.5.0", "husky": "^9.1.7", @@ -262,6 +262,23 @@ "node": ">=18" } }, + "node_modules/@boundaries/elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@boundaries/elements/-/elements-2.0.1.tgz", + "integrity": "sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "handlebars": "4.7.9", + "is-core-module": "2.16.1", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1753,13 +1770,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", @@ -2090,6 +2100,288 @@ "typescript": "*" } }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -2417,6 +2709,19 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2436,6 +2741,39 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2445,6 +2783,26 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -2731,6 +3089,137 @@ } } }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-boundaries": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-6.0.2.tgz", + "integrity": "sha512-wSHgiYeMEbziP91lH0UQ9oslgF2djG1x+LV9z/qO19ggMKZaCB8pKIGePHAY91eLF4EAgpsxQk8MRSFGRPfPzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@boundaries/elements": "2.0.1", + "chalk": "4.1.2", + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "handlebars": "4.7.9", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -2935,6 +3424,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/final-form": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/final-form/-/final-form-5.0.0.tgz", @@ -3043,6 +3545,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3076,6 +3591,38 @@ "dev": true, "license": "ISC" }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3279,6 +3826,16 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3317,6 +3874,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3776,12 +4343,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3859,6 +4420,33 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3885,6 +4473,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3910,6 +4508,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3917,6 +4531,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4436,6 +5057,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", @@ -4573,6 +5204,16 @@ "node": ">=0.10.0" } }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4702,6 +5343,19 @@ "dev": true, "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -4798,6 +5452,20 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -4825,6 +5493,41 @@ "node": ">= 10.0.0" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5112,6 +5815,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/webclient/package.json b/webclient/package.json index b2927025f..27f84e51d 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -33,7 +33,6 @@ "i18next-browser-languagedetector": "^8.2.1", "i18next-icu": "^2.0.3", "intl-messageformat": "^11.2.1", - "lodash": "^4.17.21", "prop-types": "^15.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -55,7 +54,6 @@ "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^16.3.2", "@types/dompurify": "^3.0.5", - "@types/lodash": "^4.14.179", "@types/node": "^22.19.17", "@types/prop-types": "^15.7.4", "@types/react": "^19.0.0", @@ -67,6 +65,8 @@ "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.4", "eslint": "^10.2.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^6.0.2", "fs-extra": "^11.3.4", "globals": "^17.5.0", "husky": "^9.1.7", diff --git a/webclient/src/api/AdminService.spec.ts b/webclient/src/api/AdminService.spec.ts deleted file mode 100644 index 1964a8368..000000000 --- a/webclient/src/api/AdminService.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -vi.mock('@app/websocket', () => ({ - AdminCommands: { - adjustMod: vi.fn(), - reloadConfig: vi.fn(), - shutdownServer: vi.fn(), - updateServerMessage: vi.fn(), - }, -})); - -import { AdminService } from './AdminService'; -import { AdminCommands } from '@app/websocket'; - -describe('AdminService', () => { - describe('adjustMod', () => { - it('delegates to AdminCommands.adjustMod with all arguments', () => { - AdminService.adjustMod('alice', true, false); - expect(AdminCommands.adjustMod).toHaveBeenCalledWith('alice', true, false); - }); - - it('delegates with optional arguments omitted', () => { - AdminService.adjustMod('alice'); - expect(AdminCommands.adjustMod).toHaveBeenCalledWith('alice', undefined, undefined); - }); - }); - - describe('reloadConfig', () => { - it('delegates to AdminCommands.reloadConfig', () => { - AdminService.reloadConfig(); - expect(AdminCommands.reloadConfig).toHaveBeenCalled(); - }); - }); - - describe('shutdownServer', () => { - it('delegates to AdminCommands.shutdownServer', () => { - AdminService.shutdownServer('maintenance', 10); - expect(AdminCommands.shutdownServer).toHaveBeenCalledWith('maintenance', 10); - }); - }); - - describe('updateServerMessage', () => { - it('delegates to AdminCommands.updateServerMessage', () => { - AdminService.updateServerMessage(); - expect(AdminCommands.updateServerMessage).toHaveBeenCalled(); - }); - }); -}); diff --git a/webclient/src/api/AdminService.tsx b/webclient/src/api/AdminService.tsx deleted file mode 100644 index c280fca7b..000000000 --- a/webclient/src/api/AdminService.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { AdminCommands } from '@app/websocket'; - -export class AdminService { - static adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { - AdminCommands.adjustMod(userName, shouldBeMod, shouldBeJudge); - } - - static reloadConfig(): void { - AdminCommands.reloadConfig(); - } - - static shutdownServer(reason: string, minutes: number): void { - AdminCommands.shutdownServer(reason, minutes); - } - - static updateServerMessage(): void { - AdminCommands.updateServerMessage(); - } -} diff --git a/webclient/src/api/AuthenticationService.spec.ts b/webclient/src/api/AuthenticationService.spec.ts deleted file mode 100644 index 0b2dfbc1c..000000000 --- a/webclient/src/api/AuthenticationService.spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -vi.mock('@app/websocket', () => ({ - SessionCommands: { - connect: vi.fn(), - disconnect: vi.fn(), - }, -})); - -vi.mock('../generated/proto/serverinfo_user_pb', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - ServerInfo_User_UserLevelFlag: { - IsModerator: 4, - }, - }; -}); - -import { AuthenticationService } from './AuthenticationService'; -import { SessionCommands } from '@app/websocket'; -import { App, Data } from '@app/types'; -import { create } from '@bufbuild/protobuf'; - -const baseTransport = { host: 'localhost', port: '4748' }; - -describe('AuthenticationService', () => { - describe('login', () => { - it('calls SessionCommands.connect with LOGIN reason', () => { - AuthenticationService.login({ ...baseTransport, userName: 'user', password: 'pw' }); - expect(SessionCommands.connect).toHaveBeenCalledWith( - expect.objectContaining({ - ...baseTransport, - userName: 'user', - password: 'pw', - reason: App.WebSocketConnectReason.LOGIN, - }) - ); - }); - }); - - describe('testConnection', () => { - it('calls SessionCommands.connect with TEST_CONNECTION reason', () => { - AuthenticationService.testConnection(baseTransport); - expect(SessionCommands.connect).toHaveBeenCalledWith( - expect.objectContaining({ ...baseTransport, reason: App.WebSocketConnectReason.TEST_CONNECTION }) - ); - }); - }); - - describe('register', () => { - it('calls SessionCommands.connect with REGISTER reason', () => { - AuthenticationService.register({ - ...baseTransport, - userName: 'user', - password: 'pw', - email: 'a@b.com', - country: 'US', - realName: 'User', - }); - expect(SessionCommands.connect).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'user', reason: App.WebSocketConnectReason.REGISTER }) - ); - }); - }); - - describe('activateAccount', () => { - it('calls SessionCommands.connect with ACTIVATE_ACCOUNT reason', () => { - AuthenticationService.activateAccount({ - ...baseTransport, - userName: 'user', - token: 'tok', - }); - expect(SessionCommands.connect).toHaveBeenCalledWith( - expect.objectContaining({ token: 'tok', reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }) - ); - }); - }); - - describe('resetPasswordRequest', () => { - it('calls SessionCommands.connect with PASSWORD_RESET_REQUEST reason', () => { - AuthenticationService.resetPasswordRequest({ ...baseTransport, userName: 'user' }); - expect(SessionCommands.connect).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'user', reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }) - ); - }); - }); - - describe('resetPasswordChallenge', () => { - it('calls SessionCommands.connect with PASSWORD_RESET_CHALLENGE reason', () => { - AuthenticationService.resetPasswordChallenge({ - ...baseTransport, - userName: 'user', - email: 'a@b.com', - }); - expect(SessionCommands.connect).toHaveBeenCalledWith( - expect.objectContaining({ email: 'a@b.com', reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }) - ); - }); - }); - - describe('resetPassword', () => { - it('calls SessionCommands.connect with PASSWORD_RESET reason', () => { - AuthenticationService.resetPassword({ - ...baseTransport, - userName: 'user', - token: 'tok', - newPassword: 'newpw', - }); - expect(SessionCommands.connect).toHaveBeenCalledWith( - expect.objectContaining({ newPassword: 'newpw', reason: App.WebSocketConnectReason.PASSWORD_RESET }) - ); - }); - }); - - describe('disconnect', () => { - it('delegates to SessionCommands.disconnect', () => { - AuthenticationService.disconnect(); - expect(SessionCommands.disconnect).toHaveBeenCalled(); - }); - }); - - describe('isConnected', () => { - it('returns true when state is LOGGED_IN', () => { - expect(AuthenticationService.isConnected(App.StatusEnum.LOGGED_IN)).toBe(true); - }); - - it('returns false when state is DISCONNECTED', () => { - expect(AuthenticationService.isConnected(App.StatusEnum.DISCONNECTED)).toBe(false); - }); - - it('returns false when state is CONNECTING', () => { - expect(AuthenticationService.isConnected(App.StatusEnum.CONNECTING)).toBe(false); - }); - - it('returns false when state is CONNECTED', () => { - expect(AuthenticationService.isConnected(App.StatusEnum.CONNECTED)).toBe(false); - }); - - it('returns false when state is LOGGING_IN', () => { - expect(AuthenticationService.isConnected(App.StatusEnum.LOGGING_IN)).toBe(false); - }); - }); - - describe('isModerator', () => { - it('returns true when userLevel has the IsModerator bit set', () => { - expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 4 }))).toBe(true); - }); - - it('returns true when userLevel has IsModerator and other bits set', () => { - expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 7 }))).toBe(true); - }); - - it('returns false when userLevel does not have the IsModerator bit', () => { - expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 1 }))).toBe(false); - }); - - it('returns false for admin-only userLevel without moderator bit', () => { - expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 8 }))).toBe(false); - }); - }); - - describe('isAdmin', () => { - it('returns undefined (not yet implemented)', () => { - expect(AuthenticationService.isAdmin()).toBeUndefined(); - }); - }); -}); diff --git a/webclient/src/api/AuthenticationService.tsx b/webclient/src/api/AuthenticationService.tsx deleted file mode 100644 index bacc8e350..000000000 --- a/webclient/src/api/AuthenticationService.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { App, Data, Enriched } from '@app/types'; -import { SessionCommands } from '@app/websocket'; - -export class AuthenticationService { - static login(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.LOGIN }); - } - - static testConnection(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.TEST_CONNECTION }); - } - - static register(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.REGISTER }); - } - - static activateAccount(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }); - } - - static resetPasswordRequest(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }); - } - - static resetPasswordChallenge(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); - } - - static resetPassword(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET }); - } - - static disconnect(): void { - SessionCommands.disconnect(); - } - - static isConnected(state: number): boolean { - return state === App.StatusEnum.LOGGED_IN; - } - - static isModerator(user: Data.ServerInfo_User): boolean { - const moderatorLevel = Data.ServerInfo_User_UserLevelFlag.IsModerator; - // @TODO tell cockatrice not to do this so shittily - return (user.userLevel & moderatorLevel) === moderatorLevel; - } - - static isAdmin() { - - } -} diff --git a/webclient/src/api/ModeratorService.spec.ts b/webclient/src/api/ModeratorService.spec.ts deleted file mode 100644 index f32a58d09..000000000 --- a/webclient/src/api/ModeratorService.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -vi.mock('@app/websocket', () => ({ - ModeratorCommands: { - banFromServer: vi.fn(), - getBanHistory: vi.fn(), - getWarnHistory: vi.fn(), - getWarnList: vi.fn(), - viewLogHistory: vi.fn(), - warnUser: vi.fn(), - }, -})); - -import { ModeratorService } from './ModeratorService'; -import { ModeratorCommands } from '@app/websocket'; -import { Data } from '@app/types'; - -describe('ModeratorService', () => { - describe('banFromServer', () => { - it('delegates to ModeratorCommands.banFromServer with all arguments', () => { - ModeratorService.banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible reason', 'cid', 1); - expect(ModeratorCommands.banFromServer).toHaveBeenCalledWith( - 30, 'alice', '1.2.3.4', 'reason', 'visible reason', 'cid', 1 - ); - }); - - it('delegates with only required argument', () => { - ModeratorService.banFromServer(60); - expect(ModeratorCommands.banFromServer).toHaveBeenCalledWith( - 60, undefined, undefined, undefined, undefined, undefined, undefined - ); - }); - }); - - describe('getBanHistory', () => { - it('delegates to ModeratorCommands.getBanHistory', () => { - ModeratorService.getBanHistory('alice'); - expect(ModeratorCommands.getBanHistory).toHaveBeenCalledWith('alice'); - }); - }); - - describe('getWarnHistory', () => { - it('delegates to ModeratorCommands.getWarnHistory', () => { - ModeratorService.getWarnHistory('alice'); - expect(ModeratorCommands.getWarnHistory).toHaveBeenCalledWith('alice'); - }); - }); - - describe('getWarnList', () => { - it('delegates to ModeratorCommands.getWarnList', () => { - ModeratorService.getWarnList('mod1', 'alice', 'cid123'); - expect(ModeratorCommands.getWarnList).toHaveBeenCalledWith('mod1', 'alice', 'cid123'); - }); - }); - - describe('viewLogHistory', () => { - it('delegates to ModeratorCommands.viewLogHistory', () => { - const filters: Data.ViewLogHistoryParams = { dateRange: 7, userName: 'alice' }; - ModeratorService.viewLogHistory(filters); - expect(ModeratorCommands.viewLogHistory).toHaveBeenCalledWith(filters); - }); - }); - - describe('warnUser', () => { - it('delegates to ModeratorCommands.warnUser with all arguments', () => { - ModeratorService.warnUser('alice', 'spamming', 'cid', 5); - expect(ModeratorCommands.warnUser).toHaveBeenCalledWith('alice', 'spamming', 'cid', 5); - }); - - it('delegates with only required arguments', () => { - ModeratorService.warnUser('alice', 'spamming'); - expect(ModeratorCommands.warnUser).toHaveBeenCalledWith('alice', 'spamming', undefined, undefined); - }); - }); -}); diff --git a/webclient/src/api/ModeratorService.tsx b/webclient/src/api/ModeratorService.tsx deleted file mode 100644 index 2fa9e9019..000000000 --- a/webclient/src/api/ModeratorService.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ModeratorCommands } from '@app/websocket'; -import { Data } from '@app/types'; - -export class ModeratorService { - static banFromServer(minutes: number, userName?: string, address?: string, reason?: string, - visibleReason?: string, clientid?: string, removeMessages?: number): void { - ModeratorCommands.banFromServer(minutes, userName, address, reason, visibleReason, clientid, removeMessages); - } - - static getBanHistory(userName: string): void { - ModeratorCommands.getBanHistory(userName); - } - - static getWarnHistory(userName: string): void { - ModeratorCommands.getWarnHistory(userName); - } - - static getWarnList(modName: string, userName: string, userClientid: string): void { - ModeratorCommands.getWarnList(modName, userName, userClientid); - } - - static viewLogHistory(filters: Data.ViewLogHistoryParams): void { - ModeratorCommands.viewLogHistory(filters); - } - - static warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { - ModeratorCommands.warnUser(userName, reason, clientid, removeMessages); - } -} diff --git a/webclient/src/api/RoomsService.spec.ts b/webclient/src/api/RoomsService.spec.ts deleted file mode 100644 index 80ff8d4cd..000000000 --- a/webclient/src/api/RoomsService.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -vi.mock('@app/websocket', () => ({ - SessionCommands: { - joinRoom: vi.fn(), - }, - RoomCommands: { - leaveRoom: vi.fn(), - roomSay: vi.fn(), - }, -})); - -import { RoomsService } from './RoomsService'; -import { RoomCommands, SessionCommands } from '@app/websocket'; - -describe('RoomsService', () => { - describe('joinRoom', () => { - it('delegates to SessionCommands.joinRoom', () => { - RoomsService.joinRoom(42); - expect(SessionCommands.joinRoom).toHaveBeenCalledWith(42); - }); - }); - - describe('leaveRoom', () => { - it('delegates to RoomCommands.leaveRoom', () => { - RoomsService.leaveRoom(42); - expect(RoomCommands.leaveRoom).toHaveBeenCalledWith(42); - }); - }); - - describe('roomSay', () => { - it('delegates to RoomCommands.roomSay', () => { - RoomsService.roomSay(42, 'hello room'); - expect(RoomCommands.roomSay).toHaveBeenCalledWith(42, 'hello room'); - }); - }); -}); diff --git a/webclient/src/api/RoomsService.tsx b/webclient/src/api/RoomsService.tsx deleted file mode 100644 index b2f9ddc4e..000000000 --- a/webclient/src/api/RoomsService.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { RoomCommands, SessionCommands } from '@app/websocket'; - -export class RoomsService { - static joinRoom(roomId: number): void { - SessionCommands.joinRoom(roomId); - } - - static leaveRoom(roomId: number): void { - RoomCommands.leaveRoom(roomId); - } - - static roomSay(roomId: number, message: string): void { - RoomCommands.roomSay(roomId, message); - } -} diff --git a/webclient/src/api/SessionService.spec.ts b/webclient/src/api/SessionService.spec.ts deleted file mode 100644 index 879d4c3ef..000000000 --- a/webclient/src/api/SessionService.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -vi.mock('@app/websocket', () => ({ - SessionCommands: { - addToBuddyList: vi.fn(), - removeFromBuddyList: vi.fn(), - addToIgnoreList: vi.fn(), - removeFromIgnoreList: vi.fn(), - accountPassword: vi.fn(), - accountEdit: vi.fn(), - accountImage: vi.fn(), - message: vi.fn(), - getUserInfo: vi.fn(), - getGamesOfUser: vi.fn(), - }, -})); - -import { SessionService } from './SessionService'; -import { SessionCommands } from '@app/websocket'; - -describe('SessionService', () => { - describe('addToBuddyList', () => { - it('delegates to SessionCommands.addToBuddyList', () => { - SessionService.addToBuddyList('alice'); - expect(SessionCommands.addToBuddyList).toHaveBeenCalledWith('alice'); - }); - }); - - describe('removeFromBuddyList', () => { - it('delegates to SessionCommands.removeFromBuddyList', () => { - SessionService.removeFromBuddyList('alice'); - expect(SessionCommands.removeFromBuddyList).toHaveBeenCalledWith('alice'); - }); - }); - - describe('addToIgnoreList', () => { - it('delegates to SessionCommands.addToIgnoreList', () => { - SessionService.addToIgnoreList('bob'); - expect(SessionCommands.addToIgnoreList).toHaveBeenCalledWith('bob'); - }); - }); - - describe('removeFromIgnoreList', () => { - it('delegates to SessionCommands.removeFromIgnoreList', () => { - SessionService.removeFromIgnoreList('bob'); - expect(SessionCommands.removeFromIgnoreList).toHaveBeenCalledWith('bob'); - }); - }); - - describe('changeAccountPassword', () => { - it('delegates to SessionCommands.accountPassword with all arguments', () => { - SessionService.changeAccountPassword('oldPw', 'newPw', 'hashedPw'); - expect(SessionCommands.accountPassword).toHaveBeenCalledWith('oldPw', 'newPw', 'hashedPw'); - }); - - it('delegates without hashedNewPassword when omitted', () => { - SessionService.changeAccountPassword('oldPw', 'newPw'); - expect(SessionCommands.accountPassword).toHaveBeenCalledWith('oldPw', 'newPw', undefined); - }); - }); - - describe('changeAccountDetails', () => { - it('delegates to SessionCommands.accountEdit with all arguments', () => { - SessionService.changeAccountDetails('pw', 'Alice', 'alice@example.com', 'US'); - expect(SessionCommands.accountEdit).toHaveBeenCalledWith('pw', 'Alice', 'alice@example.com', 'US'); - }); - - it('delegates with only required argument', () => { - SessionService.changeAccountDetails('pw'); - expect(SessionCommands.accountEdit).toHaveBeenCalledWith('pw', undefined, undefined, undefined); - }); - }); - - describe('changeAccountImage', () => { - it('delegates to SessionCommands.accountImage', () => { - const image = new Uint8Array([1, 2, 3]); - SessionService.changeAccountImage(image); - expect(SessionCommands.accountImage).toHaveBeenCalledWith(image); - }); - }); - - describe('sendDirectMessage', () => { - it('delegates to SessionCommands.message', () => { - SessionService.sendDirectMessage('alice', 'hello'); - expect(SessionCommands.message).toHaveBeenCalledWith('alice', 'hello'); - }); - }); - - describe('getUserInfo', () => { - it('delegates to SessionCommands.getUserInfo', () => { - SessionService.getUserInfo('alice'); - expect(SessionCommands.getUserInfo).toHaveBeenCalledWith('alice'); - }); - }); - - describe('getUserGames', () => { - it('delegates to SessionCommands.getGamesOfUser', () => { - SessionService.getUserGames('alice'); - expect(SessionCommands.getGamesOfUser).toHaveBeenCalledWith('alice'); - }); - }); -}); diff --git a/webclient/src/api/SessionService.tsx b/webclient/src/api/SessionService.tsx deleted file mode 100644 index 129cdfc58..000000000 --- a/webclient/src/api/SessionService.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { SessionCommands } from '@app/websocket'; - -export class SessionService { - static addToBuddyList(userName: string) { - SessionCommands.addToBuddyList(userName); - } - - static removeFromBuddyList(userName: string) { - SessionCommands.removeFromBuddyList(userName); - } - - static addToIgnoreList(userName: string) { - SessionCommands.addToIgnoreList(userName); - } - - static removeFromIgnoreList(userName: string) { - SessionCommands.removeFromIgnoreList(userName); - } - - static changeAccountPassword(oldPassword: string, newPassword: string, hashedNewPassword?: string): void { - SessionCommands.accountPassword(oldPassword, newPassword, hashedNewPassword); - } - - static changeAccountDetails(passwordCheck: string, realName?: string, email?: string, country?: string): void { - SessionCommands.accountEdit(passwordCheck, realName, email, country); - } - - static changeAccountImage(image: Uint8Array): void { - SessionCommands.accountImage(image); - } - - static sendDirectMessage(userName: string, message: string): void { - SessionCommands.message(userName, message); - } - - static getUserInfo(userName: string): void { - SessionCommands.getUserInfo(userName); - } - - static getUserGames(userName: string): void { - SessionCommands.getGamesOfUser(userName); - } -} diff --git a/webclient/src/api/index.ts b/webclient/src/api/index.ts index c4f67092e..651bc3162 100644 --- a/webclient/src/api/index.ts +++ b/webclient/src/api/index.ts @@ -1,5 +1,32 @@ -export { AdminService } from './AdminService'; -export { AuthenticationService } from './AuthenticationService'; -export { ModeratorService } from './ModeratorService'; -export { RoomsService } from './RoomsService'; -export { SessionService } from './SessionService'; +import { WebClient } from '@app/websocket'; +import type { IWebClientRequest } from '@app/websocket'; + +export { createWebClientResponse } from './response'; +export { createWebClientRequest } from './request'; + +/** + * UI-facing request surface. Each property is a lazy getter that resolves + * `WebClient.instance` at call time, so consumers can import this before the + * singleton is bootstrapped — it only needs to exist by the first actual call. + * + * Prefer this over importing `WebClient` directly: it keeps UI code free of + * transport-layer names and makes `@app/websocket` an internal detail of the + * `api` layer. + */ +export const request: IWebClientRequest = { + get authentication() { + return WebClient.instance.request.authentication; + }, + get session() { + return WebClient.instance.request.session; + }, + get rooms() { + return WebClient.instance.request.rooms; + }, + get admin() { + return WebClient.instance.request.admin; + }, + get moderator() { + return WebClient.instance.request.moderator; + }, +}; diff --git a/webclient/src/api/request/AdminRequestImpl.ts b/webclient/src/api/request/AdminRequestImpl.ts new file mode 100644 index 000000000..5cc21695b --- /dev/null +++ b/webclient/src/api/request/AdminRequestImpl.ts @@ -0,0 +1,20 @@ +import type { IAdminRequest } from '@app/websocket'; +import { AdminCommands } from '@app/websocket'; + +export class AdminRequestImpl implements IAdminRequest { + adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { + AdminCommands.adjustMod(userName, shouldBeMod, shouldBeJudge); + } + + reloadConfig(): void { + AdminCommands.reloadConfig(); + } + + shutdownServer(reason: string, minutes: number): void { + AdminCommands.shutdownServer(reason, minutes); + } + + updateServerMessage(): void { + AdminCommands.updateServerMessage(); + } +} diff --git a/webclient/src/api/request/AuthenticationRequestImpl.ts b/webclient/src/api/request/AuthenticationRequestImpl.ts new file mode 100644 index 000000000..9157d736f --- /dev/null +++ b/webclient/src/api/request/AuthenticationRequestImpl.ts @@ -0,0 +1,37 @@ +import { App, Enriched } from '@app/types'; +import type { IAuthenticationRequest } from '@app/websocket'; +import { SessionCommands } from '@app/websocket'; + +export class AuthenticationRequestImpl implements IAuthenticationRequest { + login(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.LOGIN }); + } + + testConnection(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.TEST_CONNECTION }); + } + + register(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.REGISTER }); + } + + activateAccount(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }); + } + + resetPasswordRequest(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }); + } + + resetPasswordChallenge(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); + } + + resetPassword(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET }); + } + + disconnect(): void { + SessionCommands.disconnect(); + } +} diff --git a/webclient/src/api/request/ModeratorRequestImpl.ts b/webclient/src/api/request/ModeratorRequestImpl.ts new file mode 100644 index 000000000..9f5c063da --- /dev/null +++ b/webclient/src/api/request/ModeratorRequestImpl.ts @@ -0,0 +1,37 @@ +import { Data } from '@app/types'; +import type { IModeratorRequest } from '@app/websocket'; +import { ModeratorCommands } from '@app/websocket'; + +export class ModeratorRequestImpl implements IModeratorRequest { + banFromServer( + minutes: number, + userName?: string, + address?: string, + reason?: string, + visibleReason?: string, + clientid?: string, + removeMessages?: number + ): void { + ModeratorCommands.banFromServer(minutes, userName, address, reason, visibleReason, clientid, removeMessages); + } + + getBanHistory(userName: string): void { + ModeratorCommands.getBanHistory(userName); + } + + getWarnHistory(userName: string): void { + ModeratorCommands.getWarnHistory(userName); + } + + getWarnList(modName: string, userName: string, userClientid: string): void { + ModeratorCommands.getWarnList(modName, userName, userClientid); + } + + viewLogHistory(filters: Data.ViewLogHistoryParams): void { + ModeratorCommands.viewLogHistory(filters); + } + + warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { + ModeratorCommands.warnUser(userName, reason, clientid, removeMessages); + } +} diff --git a/webclient/src/api/request/RoomsRequestImpl.ts b/webclient/src/api/request/RoomsRequestImpl.ts new file mode 100644 index 000000000..62b1f4e9b --- /dev/null +++ b/webclient/src/api/request/RoomsRequestImpl.ts @@ -0,0 +1,16 @@ +import type { IRoomsRequest } from '@app/websocket'; +import { RoomCommands, SessionCommands } from '@app/websocket'; + +export class RoomsRequestImpl implements IRoomsRequest { + joinRoom(roomId: number): void { + SessionCommands.joinRoom(roomId); + } + + leaveRoom(roomId: number): void { + RoomCommands.leaveRoom(roomId); + } + + roomSay(roomId: number, message: string): void { + RoomCommands.roomSay(roomId, message); + } +} diff --git a/webclient/src/api/request/SessionRequestImpl.ts b/webclient/src/api/request/SessionRequestImpl.ts new file mode 100644 index 000000000..0075b9418 --- /dev/null +++ b/webclient/src/api/request/SessionRequestImpl.ts @@ -0,0 +1,44 @@ +import type { ISessionRequest } from '@app/websocket'; +import { SessionCommands } from '@app/websocket'; + +export class SessionRequestImpl implements ISessionRequest { + addToBuddyList(userName: string): void { + SessionCommands.addToBuddyList(userName); + } + + removeFromBuddyList(userName: string): void { + SessionCommands.removeFromBuddyList(userName); + } + + addToIgnoreList(userName: string): void { + SessionCommands.addToIgnoreList(userName); + } + + removeFromIgnoreList(userName: string): void { + SessionCommands.removeFromIgnoreList(userName); + } + + changeAccountPassword(oldPassword: string, newPassword: string, hashedNewPassword?: string): void { + SessionCommands.accountPassword(oldPassword, newPassword, hashedNewPassword); + } + + changeAccountDetails(passwordCheck: string, realName?: string, email?: string, country?: string): void { + SessionCommands.accountEdit(passwordCheck, realName, email, country); + } + + changeAccountImage(image: Uint8Array): void { + SessionCommands.accountImage(image); + } + + sendDirectMessage(userName: string, message: string): void { + SessionCommands.message(userName, message); + } + + getUserInfo(userName: string): void { + SessionCommands.getUserInfo(userName); + } + + getUserGames(userName: string): void { + SessionCommands.getGamesOfUser(userName); + } +} diff --git a/webclient/src/api/request/index.ts b/webclient/src/api/request/index.ts new file mode 100644 index 000000000..85e55a739 --- /dev/null +++ b/webclient/src/api/request/index.ts @@ -0,0 +1,23 @@ +import type { IWebClientRequest } from '@app/websocket'; + +import { AuthenticationRequestImpl } from './AuthenticationRequestImpl'; +import { SessionRequestImpl } from './SessionRequestImpl'; +import { RoomsRequestImpl } from './RoomsRequestImpl'; +import { AdminRequestImpl } from './AdminRequestImpl'; +import { ModeratorRequestImpl } from './ModeratorRequestImpl'; + +export { AuthenticationRequestImpl } from './AuthenticationRequestImpl'; +export { SessionRequestImpl } from './SessionRequestImpl'; +export { RoomsRequestImpl } from './RoomsRequestImpl'; +export { AdminRequestImpl } from './AdminRequestImpl'; +export { ModeratorRequestImpl } from './ModeratorRequestImpl'; + +export function createWebClientRequest(): IWebClientRequest { + return { + authentication: new AuthenticationRequestImpl(), + session: new SessionRequestImpl(), + rooms: new RoomsRequestImpl(), + admin: new AdminRequestImpl(), + moderator: new ModeratorRequestImpl(), + }; +} diff --git a/webclient/src/api/response/AdminResponseImpl.ts b/webclient/src/api/response/AdminResponseImpl.ts new file mode 100644 index 000000000..a47b0f40b --- /dev/null +++ b/webclient/src/api/response/AdminResponseImpl.ts @@ -0,0 +1,20 @@ +import type { IAdminResponse } from '@app/websocket'; +import { ServerDispatch } from '@app/store'; + +export class AdminResponseImpl implements IAdminResponse { + adjustMod(userName: string, shouldBeMod: boolean, shouldBeJudge: boolean): void { + ServerDispatch.adjustMod(userName, shouldBeMod, shouldBeJudge); + } + + reloadConfig(): void { + ServerDispatch.reloadConfig(); + } + + shutdownServer(): void { + ServerDispatch.shutdownServer(); + } + + updateServerMessage(): void { + ServerDispatch.updateServerMessage(); + } +} diff --git a/webclient/src/api/response/GameResponseImpl.ts b/webclient/src/api/response/GameResponseImpl.ts new file mode 100644 index 000000000..cb8b9fef7 --- /dev/null +++ b/webclient/src/api/response/GameResponseImpl.ts @@ -0,0 +1,125 @@ +import { Data } from '@app/types'; +import type { IGameResponse } from '@app/websocket'; +import { GameDispatch } from '@app/store'; + +export class GameResponseImpl implements IGameResponse { + clearStore(): void { + GameDispatch.clearStore(); + } + + gameStateChanged(gameId: number, data: Data.Event_GameStateChanged): void { + GameDispatch.gameStateChanged(gameId, data); + } + + playerJoined(gameId: number, playerProperties: Data.ServerInfo_PlayerProperties): void { + GameDispatch.playerJoined(gameId, playerProperties); + } + + playerLeft(gameId: number, playerId: number, reason: number): void { + GameDispatch.playerLeft(gameId, playerId, reason); + } + + playerPropertiesChanged(gameId: number, playerId: number, properties: Data.ServerInfo_PlayerProperties): void { + GameDispatch.playerPropertiesChanged(gameId, playerId, properties); + } + + gameClosed(gameId: number): void { + GameDispatch.gameClosed(gameId); + } + + gameHostChanged(gameId: number, hostId: number): void { + GameDispatch.gameHostChanged(gameId, hostId); + } + + kicked(gameId: number): void { + GameDispatch.kicked(gameId); + } + + gameSay(gameId: number, playerId: number, message: string): void { + GameDispatch.gameSay(gameId, playerId, message); + } + + cardMoved(gameId: number, playerId: number, data: Data.Event_MoveCard): void { + GameDispatch.cardMoved(gameId, playerId, data); + } + + cardFlipped(gameId: number, playerId: number, data: Data.Event_FlipCard): void { + GameDispatch.cardFlipped(gameId, playerId, data); + } + + cardDestroyed(gameId: number, playerId: number, data: Data.Event_DestroyCard): void { + GameDispatch.cardDestroyed(gameId, playerId, data); + } + + cardAttached(gameId: number, playerId: number, data: Data.Event_AttachCard): void { + GameDispatch.cardAttached(gameId, playerId, data); + } + + tokenCreated(gameId: number, playerId: number, data: Data.Event_CreateToken): void { + GameDispatch.tokenCreated(gameId, playerId, data); + } + + cardAttrChanged(gameId: number, playerId: number, data: Data.Event_SetCardAttr): void { + GameDispatch.cardAttrChanged(gameId, playerId, data); + } + + cardCounterChanged(gameId: number, playerId: number, data: Data.Event_SetCardCounter): void { + GameDispatch.cardCounterChanged(gameId, playerId, data); + } + + arrowCreated(gameId: number, playerId: number, data: Data.Event_CreateArrow): void { + GameDispatch.arrowCreated(gameId, playerId, data); + } + + arrowDeleted(gameId: number, playerId: number, data: Data.Event_DeleteArrow): void { + GameDispatch.arrowDeleted(gameId, playerId, data); + } + + counterCreated(gameId: number, playerId: number, data: Data.Event_CreateCounter): void { + GameDispatch.counterCreated(gameId, playerId, data); + } + + counterSet(gameId: number, playerId: number, data: Data.Event_SetCounter): void { + GameDispatch.counterSet(gameId, playerId, data); + } + + counterDeleted(gameId: number, playerId: number, data: Data.Event_DelCounter): void { + GameDispatch.counterDeleted(gameId, playerId, data); + } + + cardsDrawn(gameId: number, playerId: number, data: Data.Event_DrawCards): void { + GameDispatch.cardsDrawn(gameId, playerId, data); + } + + cardsRevealed(gameId: number, playerId: number, data: Data.Event_RevealCards): void { + GameDispatch.cardsRevealed(gameId, playerId, data); + } + + zoneShuffled(gameId: number, playerId: number, data: Data.Event_Shuffle): void { + GameDispatch.zoneShuffled(gameId, playerId, data); + } + + dieRolled(gameId: number, playerId: number, data: Data.Event_RollDie): void { + GameDispatch.dieRolled(gameId, playerId, data); + } + + activePlayerSet(gameId: number, activePlayerId: number): void { + GameDispatch.activePlayerSet(gameId, activePlayerId); + } + + activePhaseSet(gameId: number, phase: number): void { + GameDispatch.activePhaseSet(gameId, phase); + } + + turnReversed(gameId: number, reversed: boolean): void { + GameDispatch.turnReversed(gameId, reversed); + } + + zoneDumped(gameId: number, playerId: number, data: Data.Event_DumpZone): void { + GameDispatch.zoneDumped(gameId, playerId, data); + } + + zonePropertiesChanged(gameId: number, playerId: number, data: Data.Event_ChangeZoneProperties): void { + GameDispatch.zonePropertiesChanged(gameId, playerId, data); + } +} diff --git a/webclient/src/api/response/ModeratorResponseImpl.ts b/webclient/src/api/response/ModeratorResponseImpl.ts new file mode 100644 index 000000000..c855152af --- /dev/null +++ b/webclient/src/api/response/ModeratorResponseImpl.ts @@ -0,0 +1,45 @@ +import { Data } from '@app/types'; +import type { IModeratorResponse } from '@app/websocket'; +import { ServerDispatch } from '@app/store'; + +export class ModeratorResponseImpl implements IModeratorResponse { + banFromServer(userName: string): void { + ServerDispatch.banFromServer(userName); + } + + banHistory(userName: string, banHistory: Data.ServerInfo_Ban[]): void { + ServerDispatch.banHistory(userName, banHistory); + } + + viewLogs(logs: Data.ServerInfo_ChatMessage[]): void { + ServerDispatch.viewLogs(logs); + } + + warnHistory(userName: string, warnHistory: Data.ServerInfo_Warning[]): void { + ServerDispatch.warnHistory(userName, warnHistory); + } + + warnListOptions(warnList: Data.Response_WarnList[]): void { + ServerDispatch.warnListOptions(warnList); + } + + warnUser(userName: string): void { + ServerDispatch.warnUser(userName); + } + + grantReplayAccess(replayId: number, moderatorName: string): void { + ServerDispatch.grantReplayAccess(replayId, moderatorName); + } + + forceActivateUser(usernameToActivate: string, moderatorName: string): void { + ServerDispatch.forceActivateUser(usernameToActivate, moderatorName); + } + + getAdminNotes(userName: string, notes: string): void { + ServerDispatch.getAdminNotes(userName, notes); + } + + updateAdminNotes(userName: string, notes: string): void { + ServerDispatch.updateAdminNotes(userName, notes); + } +} diff --git a/webclient/src/api/response/RoomResponseImpl.ts b/webclient/src/api/response/RoomResponseImpl.ts new file mode 100644 index 000000000..1a11bc4c5 --- /dev/null +++ b/webclient/src/api/response/RoomResponseImpl.ts @@ -0,0 +1,49 @@ +import { Data, Enriched } from '@app/types'; +import type { IRoomResponse } from '@app/websocket'; +import { RoomsDispatch } from '@app/store'; + +export class RoomResponseImpl implements IRoomResponse { + clearStore(): void { + RoomsDispatch.clearStore(); + } + + joinRoom(roomInfo: Data.ServerInfo_Room): void { + RoomsDispatch.joinRoom(roomInfo); + } + + leaveRoom(roomId: number): void { + RoomsDispatch.leaveRoom(roomId); + } + + updateRooms(rooms: Data.ServerInfo_Room[]): void { + RoomsDispatch.updateRooms(rooms); + } + + updateGames(roomId: number, gameList: Data.ServerInfo_Game[]): void { + RoomsDispatch.updateGames(roomId, gameList); + } + + addMessage(roomId: number, message: Enriched.Message): void { + RoomsDispatch.addMessage(roomId, message); + } + + userJoined(roomId: number, user: Data.ServerInfo_User): void { + RoomsDispatch.userJoined(roomId, user); + } + + userLeft(roomId: number, name: string): void { + RoomsDispatch.userLeft(roomId, name); + } + + removeMessages(roomId: number, name: string, amount: number): void { + RoomsDispatch.removeMessages(roomId, name, amount); + } + + gameCreated(roomId: number): void { + RoomsDispatch.gameCreated(roomId); + } + + joinedGame(roomId: number, gameId: number): void { + RoomsDispatch.joinedGame(roomId, gameId); + } +} diff --git a/webclient/src/api/response/SessionResponseImpl.ts b/webclient/src/api/response/SessionResponseImpl.ts new file mode 100644 index 000000000..98e1fbd70 --- /dev/null +++ b/webclient/src/api/response/SessionResponseImpl.ts @@ -0,0 +1,232 @@ +import { App, Data, Enriched } from '@app/types'; +import type { ISessionResponse } from '@app/websocket'; +import { GameDispatch, RoomsDispatch, ServerDispatch } from '@app/store'; + +export class SessionResponseImpl implements ISessionResponse { + initialized(): void { + ServerDispatch.initialized(); + } + + connectionAttempted(): void { + ServerDispatch.connectionAttempted(); + } + + clearStore(): void { + ServerDispatch.clearStore(); + } + + loginSuccessful(options: Enriched.LoginSuccessContext): void { + ServerDispatch.loginSuccessful(options); + } + + loginFailed(): void { + ServerDispatch.loginFailed(); + } + + connectionFailed(): void { + ServerDispatch.connectionFailed(); + } + + testConnectionSuccessful(): void { + ServerDispatch.testConnectionSuccessful(); + } + + testConnectionFailed(): void { + ServerDispatch.testConnectionFailed(); + } + + updateBuddyList(buddyList: Data.ServerInfo_User[]): void { + ServerDispatch.updateBuddyList(buddyList); + } + + addToBuddyList(user: Data.ServerInfo_User): void { + ServerDispatch.addToBuddyList(user); + } + + removeFromBuddyList(userName: string): void { + ServerDispatch.removeFromBuddyList(userName); + } + + updateIgnoreList(ignoreList: Data.ServerInfo_User[]): void { + ServerDispatch.updateIgnoreList(ignoreList); + } + + addToIgnoreList(user: Data.ServerInfo_User): void { + ServerDispatch.addToIgnoreList(user); + } + + removeFromIgnoreList(userName: string): void { + ServerDispatch.removeFromIgnoreList(userName); + } + + updateInfo(name: string, version: string): void { + ServerDispatch.updateInfo(name, version); + } + + updateStatus(state: App.StatusEnum, description: string): void { + if (state === App.StatusEnum.DISCONNECTED) { + GameDispatch.clearStore(); + RoomsDispatch.clearStore(); + ServerDispatch.clearStore(); + } + ServerDispatch.updateStatus(state, description); + } + + updateUser(user: Data.ServerInfo_User): void { + ServerDispatch.updateUser(user); + } + + updateUsers(users: Data.ServerInfo_User[]): void { + ServerDispatch.updateUsers(users); + } + + userJoined(user: Data.ServerInfo_User): void { + ServerDispatch.userJoined(user); + } + + userLeft(userName: string): void { + ServerDispatch.userLeft(userName); + } + + serverMessage(message: string): void { + ServerDispatch.serverMessage(message); + } + + accountAwaitingActivation(options: Enriched.PendingActivationContext): void { + ServerDispatch.accountAwaitingActivation(options); + } + + accountActivationSuccess(): void { + ServerDispatch.accountActivationSuccess(); + } + + accountActivationFailed(): void { + ServerDispatch.accountActivationFailed(); + } + + registrationRequiresEmail(): void { + ServerDispatch.registrationRequiresEmail(); + } + + registrationSuccess(): void { + ServerDispatch.registrationSuccess(); + } + + registrationFailed(reason: string, endTime?: number): void { + ServerDispatch.registrationFailed(reason, endTime); + } + + registrationEmailError(error: string): void { + ServerDispatch.registrationEmailError(error); + } + + registrationPasswordError(error: string): void { + ServerDispatch.registrationPasswordError(error); + } + + registrationUserNameError(error: string): void { + ServerDispatch.registrationUserNameError(error); + } + + resetPasswordChallenge(): void { + ServerDispatch.resetPasswordChallenge(); + } + + resetPassword(): void { + ServerDispatch.resetPassword(); + } + + resetPasswordSuccess(): void { + ServerDispatch.resetPasswordSuccess(); + } + + resetPasswordFailed(): void { + ServerDispatch.resetPasswordFailed(); + } + + accountPasswordChange(): void { + ServerDispatch.accountPasswordChange(); + } + + accountEditChanged(realName?: string, email?: string, country?: string): void { + ServerDispatch.accountEditChanged({ realName, email, country }); + } + + accountImageChanged(avatarBmp: Uint8Array): void { + ServerDispatch.accountImageChanged({ avatarBmp }); + } + + getUserInfo(userInfo: Data.ServerInfo_User): void { + ServerDispatch.getUserInfo(userInfo); + } + + getGamesOfUser(userName: string, response: Data.Response_GetGamesOfUser): void { + ServerDispatch.gamesOfUser(userName, response); + } + + gameJoined(gameJoinedData: Data.Event_GameJoined): void { + GameDispatch.gameJoined(gameJoinedData); + } + + notifyUser(notification: Data.Event_NotifyUser): void { + ServerDispatch.notifyUser(notification); + } + + playerPropertiesChanged(gameId: number, playerId: number, payload: Data.Event_PlayerPropertiesChanged): void { + if (payload.playerProperties) { + GameDispatch.playerPropertiesChanged(gameId, playerId, payload.playerProperties); + } + } + + serverShutdown(data: Data.Event_ServerShutdown): void { + ServerDispatch.serverShutdown(data); + } + + userMessage(messageData: Data.Event_UserMessage): void { + ServerDispatch.userMessage(messageData); + } + + addToList(list: string, userName: string): void { + ServerDispatch.addToList(list, userName); + } + + removeFromList(list: string, userName: string): void { + ServerDispatch.removeFromList(list, userName); + } + + deleteServerDeck(deckId: number): void { + ServerDispatch.deckDelete(deckId); + } + + updateServerDecks(deckList: Data.Response_DeckList): void { + ServerDispatch.backendDecks(deckList); + } + + uploadServerDeck(path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem): void { + ServerDispatch.deckUpload(path, treeItem); + } + + createServerDeckDir(path: string, dirName: string): void { + ServerDispatch.deckNewDir(path, dirName); + } + + deleteServerDeckDir(path: string): void { + ServerDispatch.deckDelDir(path); + } + + replayList(matchList: Data.ServerInfo_ReplayMatch[]): void { + ServerDispatch.replayList(matchList); + } + + replayAdded(matchInfo: Data.ServerInfo_ReplayMatch): void { + ServerDispatch.replayAdded(matchInfo); + } + + replayModifyMatch(gameId: number, doNotHide: boolean): void { + ServerDispatch.replayModifyMatch(gameId, doNotHide); + } + + replayDeleteMatch(gameId: number): void { + ServerDispatch.replayDeleteMatch(gameId); + } +} diff --git a/webclient/src/api/response/index.ts b/webclient/src/api/response/index.ts new file mode 100644 index 000000000..21941a1b0 --- /dev/null +++ b/webclient/src/api/response/index.ts @@ -0,0 +1,23 @@ +import type { IWebClientResponse } from '@app/websocket'; + +import { SessionResponseImpl } from './SessionResponseImpl'; +import { RoomResponseImpl } from './RoomResponseImpl'; +import { GameResponseImpl } from './GameResponseImpl'; +import { AdminResponseImpl } from './AdminResponseImpl'; +import { ModeratorResponseImpl } from './ModeratorResponseImpl'; + +export { SessionResponseImpl } from './SessionResponseImpl'; +export { RoomResponseImpl } from './RoomResponseImpl'; +export { GameResponseImpl } from './GameResponseImpl'; +export { AdminResponseImpl } from './AdminResponseImpl'; +export { ModeratorResponseImpl } from './ModeratorResponseImpl'; + +export function createWebClientResponse(): IWebClientResponse { + return { + session: new SessionResponseImpl(), + room: new RoomResponseImpl(), + game: new GameResponseImpl(), + admin: new AdminResponseImpl(), + moderator: new ModeratorResponseImpl(), + }; +} diff --git a/webclient/src/components/Guard/AuthGuard.tsx b/webclient/src/components/Guard/AuthGuard.tsx index bf1ff1876..4bbb8e1d5 100644 --- a/webclient/src/components/Guard/AuthGuard.tsx +++ b/webclient/src/components/Guard/AuthGuard.tsx @@ -1,14 +1,12 @@ import React from 'react'; import { Navigate } from 'react-router-dom'; -import { ServerSelectors } from '@app/store'; +import { ServerSelectors, useAppSelector } from '@app/store'; import { App } from '@app/types'; -import { useAppSelector } from '@app/store'; -import { AuthenticationService } from '@app/api'; const AuthGuard = () => { - const state = useAppSelector(s => ServerSelectors.getState(s)); - return !AuthenticationService.isConnected(state) + const isConnected = useAppSelector(ServerSelectors.getIsConnected); + return !isConnected ? :
; }; diff --git a/webclient/src/components/Guard/ModGuard.tsx b/webclient/src/components/Guard/ModGuard.tsx index 18b68adf1..96844b436 100644 --- a/webclient/src/components/Guard/ModGuard.tsx +++ b/webclient/src/components/Guard/ModGuard.tsx @@ -1,14 +1,12 @@ import React from 'react'; import { Navigate } from 'react-router-dom'; -import { ServerSelectors } from '@app/store'; -import { AuthenticationService } from '@app/api'; +import { ServerSelectors, useAppSelector } from '@app/store'; import { App } from '@app/types'; -import { useAppSelector } from '@app/store'; const ModGuard = () => { - const user = useAppSelector(state => ServerSelectors.getUser(state)); - return !AuthenticationService.isModerator(user) + const isModerator = useAppSelector(ServerSelectors.getIsUserModerator); + return !isModerator ? : <>; }; diff --git a/webclient/src/components/KnownHosts/KnownHosts.tsx b/webclient/src/components/KnownHosts/KnownHosts.tsx index 096a8bece..3efdf67ac 100644 --- a/webclient/src/components/KnownHosts/KnownHosts.tsx +++ b/webclient/src/components/KnownHosts/KnownHosts.tsx @@ -13,7 +13,7 @@ import AddIcon from '@mui/icons-material/Add'; import EditRoundedIcon from '@mui/icons-material/Edit'; import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined'; -import { AuthenticationService } from '@app/api'; +import { request } from '@app/api'; import { KnownHostDialog } from '@app/dialogs'; import { useReduxEffect } from '@app/hooks'; import { HostDTO } from '@app/services'; @@ -197,7 +197,7 @@ const KnownHosts = (props) => { setTestingConnection(TestConnection.TESTING); const options = { ...App.getHostPort(hostsState.selectedHost) }; - AuthenticationService.testConnection(options); + request.authentication.testConnection(options); } return ( diff --git a/webclient/src/components/UserDisplay/UserDisplay.tsx b/webclient/src/components/UserDisplay/UserDisplay.tsx index a19bc9912..6e38fab5c 100644 --- a/webclient/src/components/UserDisplay/UserDisplay.tsx +++ b/webclient/src/components/UserDisplay/UserDisplay.tsx @@ -6,7 +6,7 @@ import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import { Images } from '@app/images'; -import { SessionService } from '@app/api'; +import { request } from '@app/api'; import { ServerSelectors } from '@app/store'; import { App, Data } from '@app/types'; import { useAppSelector } from '@app/store'; @@ -28,23 +28,23 @@ const UserDisplay = ({ user }: UserDisplayProps) => { const handleClose = () => setPosition(null); - const isABuddy = buddyList.filter(u => u.name === user.name).length; - const isIgnored = ignoreList.filter(u => u.name === user.name).length; + const isABuddy = Boolean(buddyList[user.name]); + const isIgnored = Boolean(ignoreList[user.name]); const onAddBuddy = () => { - SessionService.addToBuddyList(user.name); + request.session.addToBuddyList(user.name); handleClose(); }; const onRemoveBuddy = () => { - SessionService.removeFromBuddyList(user.name); + request.session.removeFromBuddyList(user.name); handleClose(); }; const onAddIgnore = () => { - SessionService.addToIgnoreList(user.name); + request.session.addToIgnoreList(user.name); handleClose(); }; const onRemoveIgnore = () => { - SessionService.removeFromIgnoreList(user.name); + request.session.removeFromIgnoreList(user.name); handleClose(); }; diff --git a/webclient/src/containers/Account/Account.tsx b/webclient/src/containers/Account/Account.tsx index 8e0088d06..05d0b1161 100644 --- a/webclient/src/containers/Account/Account.tsx +++ b/webclient/src/containers/Account/Account.tsx @@ -7,7 +7,7 @@ import ListItemButton from '@mui/material/ListItemButton'; import Paper from '@mui/material/Paper'; import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from '@app/components'; -import { AuthenticationService, SessionService } from '@app/api'; +import { request } from '@app/api'; import { ServerSelectors } from '@app/store'; import Layout from '../Layout/Layout'; import { useAppSelector } from '@app/store'; @@ -18,8 +18,8 @@ import AddToIgnore from './AddToIgnore'; import './Account.css'; const Account = () => { - const buddyList = useAppSelector(state => ServerSelectors.getBuddyList(state)); - const ignoreList = useAppSelector(state => ServerSelectors.getIgnoreList(state)); + const buddyList = useAppSelector(state => ServerSelectors.getSortedBuddyList(state)); + const ignoreList = useAppSelector(state => ServerSelectors.getSortedIgnoreList(state)); const serverName = useAppSelector(state => ServerSelectors.getName(state)); const serverVersion = useAppSelector(state => ServerSelectors.getVersion(state)); const user = useAppSelector(state => ServerSelectors.getUser(state)); @@ -29,11 +29,11 @@ const Account = () => { const { t } = useTranslation(); const handleAddToBuddies = ({ userName }) => { - SessionService.addToBuddyList(userName); + request.session.addToBuddyList(userName); }; const handleAddToIgnore = ({ userName }) => { - SessionService.addToIgnoreList(userName); + request.session.addToIgnoreList(userName); }; return ( @@ -91,7 +91,13 @@ const Account = () => {

Server Name: {serverName}

Server Version: {serverVersion}

- +
diff --git a/webclient/src/containers/Layout/LeftNav.tsx b/webclient/src/containers/Layout/LeftNav.tsx index 0730011d5..b9b2d377c 100644 --- a/webclient/src/containers/Layout/LeftNav.tsx +++ b/webclient/src/containers/Layout/LeftNav.tsx @@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close'; import MailOutlineRoundedIcon from '@mui/icons-material/MailOutlineRounded'; import MenuRoundedIcon from '@mui/icons-material/MenuRounded'; -import { AuthenticationService, RoomsService } from '@app/api'; +import { request } from '@app/api'; import { CardImportDialog } from '@app/dialogs'; import { Images } from '@app/images'; import { RoomsSelectors, ServerSelectors } from '@app/store'; @@ -25,8 +25,8 @@ interface LeftNavState { const LeftNav = () => { const joinedRooms = useAppSelector(state => RoomsSelectors.getJoinedRooms(state)); - const serverState = useAppSelector(state => ServerSelectors.getState(state)); - const user = useAppSelector(state => ServerSelectors.getUser(state)); + const isConnected = useAppSelector(ServerSelectors.getIsConnected); + const isModerator = useAppSelector(ServerSelectors.getIsUserModerator); const navigate = useNavigate(); const [state, setState] = useState({ anchorEl: null, @@ -40,7 +40,7 @@ const LeftNav = () => { 'Replays', ]; - if (user && AuthenticationService.isModerator(user)) { + if (isModerator) { options = [ ...options, 'Administration', @@ -49,7 +49,7 @@ const LeftNav = () => { } setState(s => ({ ...s, options })); - }, [user]); + }, [isModerator]); const handleMenuOpen = (event) => { setState(s => ({ ...s, anchorEl: event.target })); @@ -66,7 +66,7 @@ const LeftNav = () => { const leaveRoom = (event, roomId) => { event.preventDefault(); - RoomsService.leaveRoom(roomId); + request.rooms.leaveRoom(roomId); }; const openImportCardWizard = () => { @@ -85,11 +85,11 @@ const LeftNav = () => { logo - { AuthenticationService.isConnected(serverState) && ( + { isConnected && ( ) }
- { AuthenticationService.isConnected(serverState) && ( + { isConnected && (