From f2fba541543d0ad27db8e5082a0035acbc7c8e58 Mon Sep 17 00:00:00 2001 From: Isaac Hunter Date: Tue, 30 Dec 2025 00:01:22 -0500 Subject: [PATCH 1/2] refactor to use httpService, refactor auth token in network service --- .dockerignore | 1 + Dockerfile | 9 +- Taskfile.yml | 2 +- docker-compose.yml | 2 + jest.setup.ts | 1 + package-lock.json | 253 ++++++++++++++++-- package.json | 6 +- src/app.module.ts | 15 +- src/auth/auth.interceptor.spec.ts | 8 +- src/auth/auth.interceptor.ts | 7 +- src/config/database.module.ts | 26 +- .../account-link/account-link.module.ts | 19 +- .../tests/account-link.controller.spec.ts | 21 +- .../tests/account-link.service.spec.ts | 11 +- .../juke-session/juke-session.controller.ts | 7 +- .../juke-session/juke-session.module.ts | 11 +- .../juke-session/juke-session.service.ts | 22 +- .../tests/juke-session.controller.spec.ts | 51 ++-- .../tests/juke-session.service.spec.ts | 11 +- src/jukebox/jukebox.controller.ts | 30 ++- src/jukebox/jukebox.gateway.ts | 105 +++++--- src/jukebox/jukebox.module.ts | 21 +- src/jukebox/jukebox.service.ts | 3 +- .../player/dto/player-aux-update.dto.ts | 1 + src/jukebox/player/player.service.ts | 4 +- .../player/tests/player.controller.spec.ts | 23 +- .../player/tests/player.service.spec.ts | 17 +- .../queue/tests/queue.controller.spec.ts | 17 +- src/jukebox/queue/tests/queue.service.spec.ts | 17 +- src/jukebox/tests/jukebox.controller.spec.ts | 14 +- src/jukebox/tests/jukebox.service.spec.ts | 13 +- src/network/network.module.ts | 6 +- src/network/network.service.spec.ts | 4 +- src/network/network.service.ts | 21 +- src/shared/dtos/user.dto.ts | 2 + src/spotify/spotify-auth.service.ts | 7 +- src/spotify/spotify.module.ts | 14 +- src/spotify/spotify.service.ts | 8 +- src/spotify/tests/spotify.controller.spec.ts | 4 +- src/spotify/tests/spotify.service.spec.ts | 10 +- src/track/tests/track.controller.spec.ts | 4 +- src/track/tests/track.service.spec.ts | 10 +- src/track/track.module.ts | 20 +- src/utils/guards/roles.guard.ts | 2 + src/utils/guards/token.guard.ts | 2 +- src/utils/mock/mock-user.ts | 1 + 46 files changed, 598 insertions(+), 265 deletions(-) create mode 100644 jest.setup.ts diff --git a/.dockerignore b/.dockerignore index 8b521bc..63dc9a0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,7 @@ !tsconfig.json !tsconfig.build.json !jest.config.ts +!jest.setup.ts !common/ !websocket/ !packages/ diff --git a/Dockerfile b/Dockerfile index 2933762..2ab1771 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,15 +6,20 @@ COPY ./package*.json ./ USER root -RUN npm install -g npm && \ +# RUN --mount=type=cache,target=/app/node_modules \ + # --mount=type=cache,target=/usr/local/lib/node_modules \ +RUN \ + npm install -g npm && \ npm install -g typescript && \ - npm install + # npm install -g @nestjs/cli && \ + npm ci COPY ./tsconfig*.json ./ COPY ./nest-cli.json ./ COPY ./src ./src # COPY ./packages ./packages COPY ./test ./test +COPY ./jest.setup.ts ./ RUN npm run build diff --git a/Taskfile.yml b/Taskfile.yml index c6369be..1f48202 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -9,7 +9,7 @@ tasks: desc: "Build docker image if necessary" sources: - ./Dockerfile - - ./package*.json + - ./package-lock.json run: when_changed cmds: - docker-compose build diff --git a/docker-compose.yml b/docker-compose.yml index 640b208..0092102 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,9 @@ services: volumes: - ./src:/app/src - ./package.json:/app/package.json + - ./jest.setup.ts:/app/jest.setup.ts command: npm run start:dev + user: root postgres: image: postgres:13-alpine diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 0000000..e33d81f --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1 @@ +// TODO: Put global db ops in this file diff --git a/package-lock.json b/package-lock.json index d4f5d48..35bef98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@nestjs/websockets": "^11.1.6", "@redocly/cli": "^2.0.8", "@spotify/web-api-ts-sdk": "^1.2.0", - "axios": "^1.11.0", + "axios": "^1.13.2", "cache-manager": "^7.2.0", "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", @@ -36,6 +36,7 @@ "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.26", + "typeorm-extension": "^3.7.1", "yaml": "^2.8.1" }, "devDependencies": { @@ -2698,7 +2699,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2712,7 +2712,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2722,7 +2721,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -5018,9 +5016,9 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -6125,6 +6123,12 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6221,6 +6225,12 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ebec": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ebec/-/ebec-2.3.0.tgz", + "integrity": "sha512-bt+0tSL7223VU3PSVi0vtNLZ8pO1AfWolcPPMk2a/a5H+o/ZU9ky0n3A0zhrR4qzJTN61uPsGIO4ShhOukdzxA==", + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6453,6 +6463,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/envix": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/envix/-/envix-1.5.0.tgz", + "integrity": "sha512-IOxTKT+tffjxgvX2O5nq6enbkv6kBQ/QdMy18bZWo0P0rKPvsRp2/EypIPwTvJfnmk3VdOlq/KcRSZCswefM/w==", + "license": "MIT", + "dependencies": { + "std-env": "^3.7.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7187,7 +7209,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7204,7 +7225,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7271,7 +7291,6 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -7394,6 +7413,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -9435,6 +9463,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -9831,6 +9868,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/locter": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/locter/-/locter-2.2.1.tgz", + "integrity": "sha512-Cc7mowptFl7ug5he6Iuos7aGRd9xbwTfnx1ng4AX/7F4iqemPaXAIJDi13IBwQZrKgli9OPEYXm6uCKr7ynxUQ==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "ebec": "^2.3.0", + "fast-glob": "^3.3.3", + "flat": "^5.0.2", + "jiti": "^2.6.1", + "yaml": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -10026,6 +10080,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -10157,7 +10220,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -10177,7 +10239,6 @@ "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", @@ -10191,7 +10252,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -10546,6 +10606,16 @@ "node": ">=16" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -10997,6 +11067,16 @@ "node": ">= 0.8" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -11599,6 +11679,25 @@ "node": ">= 0.6" } }, + "node_modules/rapiq": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/rapiq/-/rapiq-0.9.0.tgz", + "integrity": "sha512-k4oT4RarFBrlLMJ49xUTeQpa/us0uU4I70D/UEnK3FWQ4GENzei01rEQAmvPKAIzACo4NMW+YcYJ7EVfSa7EFg==", + "license": "MIT", + "dependencies": { + "ebec": "^1.1.0", + "smob": "^1.4.0" + } + }, + "node_modules/rapiq/node_modules/ebec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ebec/-/ebec-1.1.1.tgz", + "integrity": "sha512-JZ1vcvPQtR+8LGbZmbjG21IxLQq/v47iheJqn2F6yB2CgnGfn8ZVg3myHrf3buIZS8UCwQK0jOSIb3oHX7aH8g==", + "license": "MIT", + "dependencies": { + "smob": "^1.4.0" + } + }, "node_modules/raw-body": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", @@ -11901,7 +12000,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -11944,7 +12042,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -12423,6 +12520,12 @@ "node": ">=8.0.0" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "license": "MIT" + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -12717,6 +12820,12 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, "node_modules/stickyfill": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stickyfill/-/stickyfill-1.1.1.tgz", @@ -13766,6 +13875,119 @@ } } }, + "node_modules/typeorm-extension": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/typeorm-extension/-/typeorm-extension-3.7.1.tgz", + "integrity": "sha512-SwpLlrMkRLvDSUtDbM4XM0SxhedPFW24i5sDmUV2sLogoMje0lOE5f3t3BKHR4apKUYaAxq+TiL9BhIqXx3bUA==", + "license": "MIT", + "dependencies": { + "@faker-js/faker": "^8.4.1", + "consola": "^3.4.0", + "envix": "^1.5.0", + "locter": "^2.1.6", + "pascal-case": "^3.1.2", + "rapiq": "^0.9.0", + "reflect-metadata": "^0.2.2", + "smob": "^1.5.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm-extension": "bin/cli.cjs", + "typeorm-extension-esm": "bin/cli.mjs" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "typeorm": "~0.3.0" + } + }, + "node_modules/typeorm-extension/node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, + "node_modules/typeorm-extension/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/typeorm-extension/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typeorm-extension/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typeorm-extension/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/typeorm-extension/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/typeorm/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -14678,6 +14900,7 @@ }, "packages/jukebox-types": { "version": "0.1.4", + "dev": true, "license": "ISC", "devDependencies": {} } diff --git a/package.json b/package.json index 7069f4e..e78dfef 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@nestjs/websockets": "^11.1.6", "@redocly/cli": "^2.0.8", "@spotify/web-api-ts-sdk": "^1.2.0", - "axios": "^1.11.0", + "axios": "^1.13.2", "cache-manager": "^7.2.0", "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", @@ -49,6 +49,7 @@ "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.26", + "typeorm-extension": "^3.7.1", "yaml": "^2.8.1" }, "devDependencies": { @@ -112,6 +113,9 @@ ], "testPathIgnorePatterns": [ "_deprecated" + ], + "setupFilesAfterEnv": [ + "/jest.setup.ts" ] }, "workspaces": [ diff --git a/src/app.module.ts b/src/app.module.ts index 4d49fe3..4950154 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,3 +1,4 @@ +import { HttpModule } from '@nestjs/axios' import { CacheModule } from '@nestjs/cache-manager' import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' @@ -10,26 +11,22 @@ import { JukeboxModule } from './jukebox/jukebox.module' import { NetworkModule } from './network/network.module' import { SpotifyModule } from './spotify/spotify.module' import { TrackModule } from './track/track.module' -import { AxiosProvider } from './utils/mock/mock-axios-provider' @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), CacheModule.registerAsync(CacheOptions), + HttpModule.register({ + timeout: 5000, + maxRedirects: 5, + }), DatabaseModule, NetworkModule, SpotifyModule, JukeboxModule, TrackModule, ], - // controllers: [AppController], - providers: [ - AppService, - AppGateway, - AxiosProvider, - // NetworkService, - // { provide: APP_INTERCEPTOR, useClass: AuthInterceptor }, - ], + providers: [AppService, AppGateway], controllers: [AppController], }) export class AppModule {} diff --git a/src/auth/auth.interceptor.spec.ts b/src/auth/auth.interceptor.spec.ts index 218d097..e4301f9 100644 --- a/src/auth/auth.interceptor.spec.ts +++ b/src/auth/auth.interceptor.spec.ts @@ -1,13 +1,11 @@ -import type { AxiosStatic } from 'axios' +import { HttpService } from '@nestjs/axios' import { NetworkService } from 'src/network/network.service' import { AuthInterceptor } from './auth.interceptor' describe('AuthInterceptor', () => { it('should be defined', async () => { - const axios: AxiosStatic = { - request: async (config: any) => ({ data: {}, status: 200 }) as any, - } as AxiosStatic - const networkService = new NetworkService(axios) + const httpService = new HttpService() + const networkService = new NetworkService(httpService) const interceptor = new AuthInterceptor(networkService) expect(interceptor).toBeDefined() diff --git a/src/auth/auth.interceptor.ts b/src/auth/auth.interceptor.ts index b9845cd..a4b351f 100644 --- a/src/auth/auth.interceptor.ts +++ b/src/auth/auth.interceptor.ts @@ -2,16 +2,15 @@ import { CallHandler, ExecutionContext, Injectable, - Logger, NestInterceptor, UnauthorizedException, } from '@nestjs/common' import { Request } from 'express' import { Observable } from 'rxjs' import { NODE_ENV } from 'src/config' +import { UserDto } from 'src/shared' import { mockUser } from 'src/utils/mock' import { NetworkService } from '../network/network.service' -import { UserDto } from 'src/shared' @Injectable() export class AuthInterceptor implements NestInterceptor { @@ -29,8 +28,8 @@ export class AuthInterceptor implements NestInterceptor { const [_, token] = authHeader.split(' ') try { - this.network.setToken(token) - request.user = await this.network.fetchUser() + // this.network.setToken(token) + request.user = await this.network.fetchUser(token) return next.handle() } catch (e) { throw new UnauthorizedException('Could not validate user token', e) diff --git a/src/config/database.module.ts b/src/config/database.module.ts index 2e9b6cc..c3afc73 100644 --- a/src/config/database.module.ts +++ b/src/config/database.module.ts @@ -2,6 +2,20 @@ import { Global, Logger, Module } from '@nestjs/common' import { DataSource } from 'typeorm' import { DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER } from './constants' +export const getDb = () => { + return new DataSource({ + type: 'postgres', + host: DB_HOST, + port: DB_PORT, + password: DB_PASS, + username: DB_USER, + entities: [`${__dirname}/../**/**.entity{.ts,.js}`], + database: DB_NAME, + synchronize: true, // TODO: Setup production migrations + logging: false, + }) +} + @Global() @Module({ imports: [], @@ -12,17 +26,7 @@ import { DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER } from './constants' inject: [], useFactory: async () => { try { - const db = new DataSource({ - type: 'postgres', - host: DB_HOST, - port: DB_PORT, - password: DB_PASS, - username: DB_USER, - entities: [`${__dirname}/../**/**.entity{.ts,.js}`], - database: DB_NAME, - synchronize: true, // TODO: Setup production migrations - logging: false, - }) + const db = getDb() await db.initialize() Logger.log('Database connected successfully.', 'Config Database') diff --git a/src/jukebox/account-link/account-link.module.ts b/src/jukebox/account-link/account-link.module.ts index fda1fc8..c00df8d 100644 --- a/src/jukebox/account-link/account-link.module.ts +++ b/src/jukebox/account-link/account-link.module.ts @@ -1,9 +1,8 @@ import { Module } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' -import { NetworkService } from 'src/network/network.service' +import { NetworkModule } from 'src/network/network.module' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { AxiosProvider } from 'src/utils/mock' +import { SpotifyModule } from 'src/spotify/spotify.module' import { Jukebox } from '../entities/jukebox.entity' import { JukeboxService } from '../jukebox.service' import { AccountLinkController } from './account-link.controller' @@ -11,15 +10,13 @@ import { AccountLinkService } from './account-link.service' import { AccountLink } from './entities/account-link.entity' @Module({ - imports: [TypeOrmModule.forFeature([AccountLink, SpotifyAccount, Jukebox])], - controllers: [AccountLinkController], - providers: [ - AccountLinkService, - SpotifyAuthService, - JukeboxService, - NetworkService, - AxiosProvider, + imports: [ + NetworkModule, + SpotifyModule, + TypeOrmModule.forFeature([AccountLink, SpotifyAccount, Jukebox]), ], + controllers: [AccountLinkController], + providers: [AccountLinkService, JukeboxService], exports: [AccountLinkService], }) export class AccountLinkModule {} diff --git a/src/jukebox/account-link/tests/account-link.controller.spec.ts b/src/jukebox/account-link/tests/account-link.controller.spec.ts index b421bc2..c188b34 100644 --- a/src/jukebox/account-link/tests/account-link.controller.spec.ts +++ b/src/jukebox/account-link/tests/account-link.controller.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import { NotFoundException } from '@nestjs/common' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' @@ -10,12 +11,11 @@ import { NetworkService } from 'src/network/network.service' import type { CreateSpotifyAccountDto } from 'src/spotify/dto' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { MockAxiosProvider, MockCacheProvider, mockSpotifyAccount } from 'src/utils/mock' +import { MockCacheProvider, mockSpotifyAccount } from 'src/utils/mock' import { AccountLinkController } from '../account-link.controller' import { AccountLinkService } from '../account-link.service' import type { CreateAccountLinkDto } from '../dto' import { AccountLink } from '../entities/account-link.entity' -import { DataSource } from 'typeorm' describe('AccountLinkController', () => { let module: TestingModule @@ -49,12 +49,18 @@ describe('AccountLinkController', () => { imports: [DatabaseModule, TypeOrmModule.forFeature([AccountLink, SpotifyAccount, Jukebox])], controllers: [AccountLinkController], providers: [ - MockAxiosProvider, MockCacheProvider, AccountLinkService, - SpotifyAuthService, JukeboxService, - NetworkService, + SpotifyAuthService, + { + provide: NetworkService, + useValue: {}, + }, + { + provide: HttpService, + useValue: {}, + }, ], }).compile() @@ -76,11 +82,6 @@ describe('AccountLinkController', () => { jukeboxId4 = jukebox4.id }) - afterEach(async () => { - const datasource = module.get(DataSource) - await datasource.dropDatabase() - }) - it('should be defined', () => { expect(controller).toBeDefined() }) diff --git a/src/jukebox/account-link/tests/account-link.service.spec.ts b/src/jukebox/account-link/tests/account-link.service.spec.ts index 0e8d046..91ab186 100644 --- a/src/jukebox/account-link/tests/account-link.service.spec.ts +++ b/src/jukebox/account-link/tests/account-link.service.spec.ts @@ -1,10 +1,10 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { MockAxiosProvider } from 'src/utils/mock' import { AccountLinkService } from '../account-link.service' import { AccountLink } from '../entities/account-link.entity' @@ -14,7 +14,14 @@ describe('AccountLinkService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [DatabaseModule, TypeOrmModule.forFeature([AccountLink, SpotifyAccount])], - providers: [MockAxiosProvider, AccountLinkService, SpotifyAuthService], + providers: [ + AccountLinkService, + SpotifyAuthService, + { + provide: HttpService, + useValue: {}, + }, + ], }).compile() service = module.get(AccountLinkService) diff --git a/src/jukebox/juke-session/juke-session.controller.ts b/src/jukebox/juke-session/juke-session.controller.ts index efe59ff..0e5477a 100644 --- a/src/jukebox/juke-session/juke-session.controller.ts +++ b/src/jukebox/juke-session/juke-session.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, - DefaultValuePipe, Delete, Get, Param, @@ -15,12 +14,11 @@ import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger' import { AuthInterceptor } from 'src/auth/auth.interceptor' import { NumberPipe } from 'src/pipes/int-pipe.pipe' import { UserDto } from 'src/shared' -import { Serialize } from 'src/utils' import { CurrentUser } from 'src/utils/decorators' import { Roles } from 'src/utils/decorators/roles.decorator' import { RolesGuard } from 'src/utils/guards/roles.guard' import { CreateJukeSessionDto, JukeSessionDto, UpdateJukeSessionDto } from './dto/juke-session.dto' -import { CreateJukeSessionMembershipDto, JukeSessionMembershipDto } from './dto/membership.dto' +import { CreateJukeSessionMembershipDto } from './dto/membership.dto' import { JukeSessionService } from './juke-session.service' @ApiTags('JukeSession') @@ -37,8 +35,9 @@ export class JukeSessionController { create( @Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number, @Body() createJukeSessionDto: CreateJukeSessionDto, + @CurrentUser() user: UserDto, ) { - return this.jukeSessionService.create(jukeboxId, createJukeSessionDto) + return this.jukeSessionService.create(jukeboxId, createJukeSessionDto, user.token) } @Roles('admin') diff --git a/src/jukebox/juke-session/juke-session.module.ts b/src/jukebox/juke-session/juke-session.module.ts index 47de304..f21c68c 100644 --- a/src/jukebox/juke-session/juke-session.module.ts +++ b/src/jukebox/juke-session/juke-session.module.ts @@ -1,18 +1,17 @@ import { Module } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' +import { NetworkModule } from 'src/network/network.module' +import { Jukebox } from '../entities/jukebox.entity' +import { JukeboxService } from '../jukebox.service' import { JukeSession } from './entities/juke-session.entity' import { JukeSessionMembership } from './entities/membership.entity' import { JukeSessionController } from './juke-session.controller' import { JukeSessionService } from './juke-session.service' -import { NetworkService } from 'src/network/network.service' -import { AxiosProvider } from 'src/utils/mock' -import { JukeboxService } from '../jukebox.service' -import { Jukebox } from '../entities/jukebox.entity' @Module({ - imports: [TypeOrmModule.forFeature([JukeSession, JukeSessionMembership, Jukebox])], + imports: [NetworkModule, TypeOrmModule.forFeature([JukeSession, JukeSessionMembership, Jukebox])], controllers: [JukeSessionController], - providers: [JukeSessionService, NetworkService, AxiosProvider, JukeboxService], + providers: [JukeSessionService, JukeboxService], exports: [JukeSessionService], }) export class JukeSessionModule {} diff --git a/src/jukebox/juke-session/juke-session.service.ts b/src/jukebox/juke-session/juke-session.service.ts index 6e71a16..3e382d3 100644 --- a/src/jukebox/juke-session/juke-session.service.ts +++ b/src/jukebox/juke-session/juke-session.service.ts @@ -32,7 +32,11 @@ export class JukeSessionService { // MARK: CRUD Ops // ============================================ - async create(jukeboxId: number, payload: CreateJukeSessionDto): Promise { + async create( + jukeboxId: number, + payload: CreateJukeSessionDto, + token: string, + ): Promise { // Deactivate session if it should be expired try { const activeSession = await this.getCurrentSession(jukeboxId) @@ -101,6 +105,7 @@ export class JukeSessionService { jukeboxId, createdSession.join_code, createdSession.jukebox.club_id, + token, ), }, ) @@ -113,8 +118,10 @@ export class JukeSessionService { jukeboxId: number, joinCode: string, clubId: number, + token: string, ): Promise { const link = await this.networkService.sendRequest( + token, `${CLUBS_URL}/api/v1/analytics/links/`, 'POST', { @@ -131,6 +138,7 @@ export class JukeSessionService { } const qrCode = await this.networkService.sendRequest( + token, `${CLUBS_URL}/api/v1/analytics/qrcode/`, 'POST', { @@ -327,4 +335,16 @@ export class JukeSessionService { return plainToInstance(JukeSessionDto, session) } + + /** + * Get current JukeSession or return null. + */ + async maybeGetCurrentSession(jukeboxId: number): Promise { + try { + const session = await this.getCurrentSession(jukeboxId) + return session + } catch (e) { + return null + } + } } diff --git a/src/jukebox/juke-session/tests/juke-session.controller.spec.ts b/src/jukebox/juke-session/tests/juke-session.controller.spec.ts index 4a991f1..0c20d80 100644 --- a/src/jukebox/juke-session/tests/juke-session.controller.spec.ts +++ b/src/jukebox/juke-session/tests/juke-session.controller.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import { InternalServerErrorException, NotFoundException } from '@nestjs/common' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' @@ -14,18 +15,18 @@ import { QueuedTrack } from 'src/jukebox/queue/entities/queued-track.entity' import { QueueService } from 'src/jukebox/queue/queue.service' import { NetworkService } from 'src/network/network.service' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { SpotifyService } from 'src/spotify/spotify.service' import type { TrackDto } from 'src/track/dto/track.dto' import { Track } from 'src/track/entities/track.entity' import { TrackService } from 'src/track/track.service' -import { MockAxiosProvider, MockCacheProvider } from 'src/utils/mock' -import type { CreateJukeSessionDto, JukeSessionDto } from '../dto/juke-session.dto' +import { MockCacheProvider, mockUser } from 'src/utils/mock' +import { DataSource } from 'typeorm' +import type { CreateJukeSessionDto } from '../dto/juke-session.dto' import { JukeSession } from '../entities/juke-session.entity' import { JukeSessionMembership } from '../entities/membership.entity' import { JukeSessionController } from '../juke-session.controller' import { JukeSessionService } from '../juke-session.service' -import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { DataSource } from 'typeorm' const getEndAtDate = (hours = 2) => new Date(new Date().getTime() + 1000 * 60 * 60 * hours) @@ -62,10 +63,14 @@ describe('JukeSessionController', () => { } } - const session = await jukeSessionService.create(jukeboxId, { - end_at: getEndAtDate(), - ...(payload ?? {}), - }) + const session = await jukeSessionService.create( + jukeboxId, + { + end_at: getEndAtDate(), + ...(payload ?? {}), + }, + mockUser.token, + ) return session } @@ -86,13 +91,19 @@ describe('JukeSessionController', () => { ], controllers: [JukeSessionController], providers: [ - MockAxiosProvider, MockCacheProvider, JukeSessionService, PlayerService, SpotifyService, QueueService, - NetworkService, + { + provide: NetworkService, + useValue: {}, + }, + { + provide: HttpService, + useValue: { axiosRef: { post: async (...args) => {} } }, + }, JukeboxService, TrackService, AccountLinkService, @@ -134,15 +145,23 @@ describe('JukeSessionController', () => { }) it('should create a juke session', async () => { - const result = await controller.create(jukebox.id, { - end_at: getEndAtDate(), - }) + const result = await controller.create( + jukebox.id, + { + end_at: getEndAtDate(), + }, + mockUser, + ) // Cannot create juke session when one is active await expect( - controller.create(jukebox.id, { - end_at: getEndAtDate(), - }), + controller.create( + jukebox.id, + { + end_at: getEndAtDate(), + }, + mockUser, + ), ).rejects.toThrow(InternalServerErrorException) await controller.update(result.id, jukebox.id, { is_active: false }) diff --git a/src/jukebox/juke-session/tests/juke-session.service.spec.ts b/src/jukebox/juke-session/tests/juke-session.service.spec.ts index 767381b..13dd004 100644 --- a/src/jukebox/juke-session/tests/juke-session.service.spec.ts +++ b/src/jukebox/juke-session/tests/juke-session.service.spec.ts @@ -2,11 +2,10 @@ import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' +import { NetworkService } from 'src/network/network.service' import { JukeSession } from '../entities/juke-session.entity' import { JukeSessionMembership } from '../entities/membership.entity' import { JukeSessionService } from '../juke-session.service' -import { NetworkService } from 'src/network/network.service' -import { AxiosProvider } from 'src/utils/mock' describe('JukeSessionService', () => { let service: JukeSessionService @@ -14,7 +13,13 @@ describe('JukeSessionService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [DatabaseModule, TypeOrmModule.forFeature([JukeSession, JukeSessionMembership])], - providers: [JukeSessionService, NetworkService, AxiosProvider], + providers: [ + JukeSessionService, + { + provide: NetworkService, + useValue: {}, + }, + ], }).compile() service = module.get(JukeSessionService) diff --git a/src/jukebox/jukebox.controller.ts b/src/jukebox/jukebox.controller.ts index 30f1945..9240e79 100644 --- a/src/jukebox/jukebox.controller.ts +++ b/src/jukebox/jukebox.controller.ts @@ -1,14 +1,29 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common' -import { CreateJukeboxDto, UpdateJukeboxDto } from './dto/jukebox.dto' -import { JukeboxService } from './jukebox.service' +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + UseGuards, + UseInterceptors, +} from '@nestjs/common' import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' -import { RolesGuard } from 'src/utils/guards/roles.guard' +import { AuthInterceptor } from 'src/auth/auth.interceptor' import { NumberPipe } from 'src/pipes/int-pipe.pipe' +import { UserDto } from 'src/shared' +import { CurrentUser } from 'src/utils/decorators' import { Roles } from 'src/utils/decorators/roles.decorator' +import { RolesGuard } from 'src/utils/guards/roles.guard' +import { CreateJukeboxDto, UpdateJukeboxDto } from './dto/jukebox.dto' +import { JukeboxService } from './jukebox.service' @ApiTags('Jukebox') @ApiBearerAuth() @Controller('jukebox/jukeboxes/') +@UseInterceptors(AuthInterceptor) export class JukeboxController { constructor(private readonly jukeboxService: JukeboxService) {} @@ -24,8 +39,11 @@ export class JukeboxController { @UseGuards(RolesGuard) @Get() @ApiOperation({ summary: '[MEMBER] Find all jukeboxes for a club id' }) - findAll(@Query('club_id', new NumberPipe('clubId')) clubId: number) { - return this.jukeboxService.findAll(clubId) + findAll( + @Query('club_id', new NumberPipe('clubId')) clubId: number, + @CurrentUser() user: UserDto, + ) { + return this.jukeboxService.findAll(clubId, user.token) } @Roles('member') diff --git a/src/jukebox/jukebox.gateway.ts b/src/jukebox/jukebox.gateway.ts index a5b7d11..bbe5886 100644 --- a/src/jukebox/jukebox.gateway.ts +++ b/src/jukebox/jukebox.gateway.ts @@ -1,19 +1,8 @@ +import { HttpService } from '@nestjs/axios' +import { NotFoundException, UseFilters, UsePipes, ValidationPipe } from '@nestjs/common' import { - BadRequestException, - ForbiddenException, - NotFoundException, - NotImplementedException, - UnauthorizedException, - UseFilters, - UsePipes, - ValidationPipe, -} from '@nestjs/common' -import { - BaseWsExceptionFilter, ConnectedSocket, MessageBody, - OnGatewayConnection, - OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, @@ -21,16 +10,16 @@ import { WsException, } from '@nestjs/websockets' import { Server, Socket } from 'socket.io' -import { TrackService } from 'src/track/track.service' -import { PlayerAuxUpdateDto, PlayerJoinDto } from './player/dto' -import { PlayerService } from './player/player.service' -import { QueueService } from './queue/queue.service' -import { JukeSessionService } from './juke-session/juke-session.service' import { CLUBS_URL, NODE_ENV } from 'src/config' -import { JukeboxService } from './jukebox.service' import { NetworkService } from 'src/network/network.service' +import { TrackService } from 'src/track/track.service' import { WSExceptionFilter } from 'src/utils/filters/ws-exception.filter' +import { JukeSessionService } from './juke-session/juke-session.service' +import { JukeboxService } from './jukebox.service' +import { PlayerAuxUpdateDto, PlayerJoinDto } from './player/dto' +import { PlayerService } from './player/player.service' import { QueuedTrackDto } from './queue/dto' +import { QueueService } from './queue/queue.service' @UseFilters(new WSExceptionFilter()) @WebSocketGateway({ @@ -40,23 +29,40 @@ import { QueuedTrackDto } from './queue/dto' }) @UsePipes(new ValidationPipe({ transform: true })) export class JukeboxGateway implements OnGatewayInit { + @WebSocketServer() server: Server + constructor( private jukeSessionService: JukeSessionService, private jukeboxService: JukeboxService, private playerService: PlayerService, private queueService: QueueService, private tracksService: TrackService, + private httpService: HttpService, private networkService: NetworkService, ) {} + // afterInit(server: Server) { + // server.use(async (client: Socket, next) => {}) + // } + // handleConnection(socket: Socket, ...args: any[]) { + // console.log('class http:', this.httpService) + // // let http = new HttpService() + // // console.log('new http:', http) - @WebSocketServer() server: Server + // } + // @WebSocketServer() server: Server async afterInit(server: Server) { server.use(async (client: Socket, next) => { try { if (NODE_ENV === 'dev') return next() - const authToken = client.handshake.auth?.token ?? null + let authToken = client.handshake.auth?.token ?? null + + const headerToken = client.handshake.headers.authorization + if (!authToken && headerToken) { + authToken = headerToken.split(' ')[1] + } + if (!authToken) { return next( this.handleConnectionRejection( @@ -91,6 +97,7 @@ export class JukeboxGateway implements OnGatewayInit { } const clubs = (await this.networkService.sendRequest( + authToken, `${CLUBS_URL}/api/v1/club/clubs/${role === 'admin' ? '?is_admin=true' : ''}`, 'GET', )) as { status: number; description: string; data: { id: number; name: string }[] } @@ -133,13 +140,14 @@ export class JukeboxGateway implements OnGatewayInit { @SubscribeMessage('player-join') async handlePlayerJoin(@ConnectedSocket() client: Socket, @MessageBody() payload: PlayerJoinDto) { + console.log('http in join:', this.httpService) if (client['role'] !== 'member' && client['role'] !== 'admin') { throw new WsException('You are not authorized') } const jukeboxId = payload.jukebox_id.toString() console.log('Joining ', jukeboxId) - client.join(jukeboxId) + await client.join(jukeboxId) const playerState = await this.playerService.getPlayerState(+jukeboxId) console.log(playerState) this.server.to(jukeboxId).emit('player-join-success', playerState) @@ -159,14 +167,13 @@ export class JukeboxGateway implements OnGatewayInit { @ConnectedSocket() client: Socket, @MessageBody() payload: PlayerAuxUpdateDto, ) { - console.log('Received Player Aux Update') + console.log('Received Player Aux Update:', payload) if (client['role'] !== 'admin') { throw new WsException('You are not authorized') } - console.log(payload) const { jukebox_id, action, progress, spotify_track, duration_ms } = payload - const session = await this.jukeSessionService.getCurrentSession(jukebox_id) + const session = await this.jukeSessionService.maybeGetCurrentSession(jukebox_id) switch (action) { case 'played': @@ -175,31 +182,43 @@ export class JukeboxGateway implements OnGatewayInit { case 'paused': this.playerService.setIsPlaying(jukebox_id, false) break + case 'progress': + this.playerService.setCurrentProgress(jukebox_id, progress ?? 0) case 'changed_tracks': if (spotify_track && !spotify_track?.spotify_id) { throw new WsException('Track must have a spotify id') } - // Check if next track was next in queue, if so pop it - let nextTrack: QueuedTrackDto | null - try { - nextTrack = await this.queueService.getNextTrack(session.id) - } catch (err) { - if (err instanceof NotFoundException) { - nextTrack = null - } else throw new err() + if (!session) { + if (!spotify_track) { + this.playerService.setCurrentSpotifyTrack(jukebox_id, null) + } else { + const track = await this.tracksService.getTrack(spotify_track?.spotify_id, jukebox_id) + this.playerService.setCurrentSpotifyTrack(jukebox_id, track) + } + } else { + // Check if next track was next in queue, if so pop it + let nextTrack: QueuedTrackDto | null + try { + nextTrack = await this.queueService.getNextTrack(session.id) + } catch (err) { + if (err instanceof NotFoundException) { + nextTrack = null + } else throw new err() + } + + if (nextTrack && nextTrack.track.spotify_id === spotify_track?.spotify_id) { + // Changed track was next from queue + const track = await this.queueService.popNextTrack(session.id) + await this.playerService.setCurrentQueuedTrack(jukebox_id, track) + await this.queueService.queueNextTrackToSpotify(jukebox_id, session.id) + } else if (spotify_track) { + // Changed track was outside of queue + const track = await this.tracksService.getTrack(spotify_track.spotify_id!, jukebox_id) + await this.playerService.setCurrentSpotifyTrack(jukebox_id, track) + } } - if (nextTrack && nextTrack.track.spotify_id === spotify_track?.spotify_id) { - // Changed track was next from queue - const track = await this.queueService.popNextTrack(session.id) - await this.playerService.setCurrentQueuedTrack(jukebox_id, track) - await this.queueService.queueNextTrackToSpotify(jukebox_id, session.id) - } else if (spotify_track) { - // Changed track was outside of queue - const track = await this.tracksService.getTrack(spotify_track.spotify_id!, jukebox_id) - await this.playerService.setCurrentSpotifyTrack(jukebox_id, track) - } break default: throw new WsException('Unknown Socket Player Action') diff --git a/src/jukebox/jukebox.module.ts b/src/jukebox/jukebox.module.ts index dbb9d4b..90a9c36 100644 --- a/src/jukebox/jukebox.module.ts +++ b/src/jukebox/jukebox.module.ts @@ -1,9 +1,13 @@ import { forwardRef, Module } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' +import { NetworkModule } from 'src/network/network.module' import { SpotifyModule } from '../spotify/spotify.module' +import { TrackModule } from '../track/track.module' import { AccountLinkModule } from './account-link/account-link.module' +import { Jukebox } from './entities/jukebox.entity' import { JukeSessionModule } from './juke-session/juke-session.module' import { JukeboxController } from './jukebox.controller' +import { JukeboxGateway } from './jukebox.gateway' import { JukeboxService } from './jukebox.service' import { PlayerInteraction } from './player/entity/player-interaction.entity' import { PlayerController } from './player/player.controller' @@ -11,29 +15,20 @@ import { PlayerService } from './player/player.service' import { QueuedTrack } from './queue/entities/queued-track.entity' import { QueueController } from './queue/queue.controller' import { QueueService } from './queue/queue.service' -import { Jukebox } from './entities/jukebox.entity' -import { TrackModule } from '../track/track.module' -import { NetworkService } from '../network/network.service' -import { AxiosProvider } from '../utils/mock' -import { JukeboxGateway } from './jukebox.gateway' +import { HttpModule, HttpService } from '@nestjs/axios' @Module({ imports: [ AccountLinkModule, JukeSessionModule, TrackModule, + NetworkModule, + HttpModule, forwardRef(() => SpotifyModule), TypeOrmModule.forFeature([Jukebox, PlayerInteraction, QueuedTrack]), ], controllers: [JukeboxController, QueueController, PlayerController], - providers: [ - AxiosProvider, - JukeboxService, - QueueService, - PlayerService, - NetworkService, - JukeboxGateway, - ], + providers: [JukeboxService, QueueService, PlayerService, JukeboxGateway], exports: [JukeboxService, QueueService, PlayerService, JukeSessionModule], }) export class JukeboxModule {} diff --git a/src/jukebox/jukebox.service.ts b/src/jukebox/jukebox.service.ts index 7df525f..e8aa30d 100644 --- a/src/jukebox/jukebox.service.ts +++ b/src/jukebox/jukebox.service.ts @@ -20,11 +20,12 @@ export class JukeboxService { return plainToInstance(JukeboxDto, jukebox) } - async findAll(clubId: number): Promise { + async findAll(clubId: number, authToken: string): Promise { let result = await this.jukeboxRepo.find({ where: { club_id: clubId } }) if (result.length === 0) { const clubs = (await this.networkService.sendRequest( + authToken, `${CLUBS_URL}/api/v1/club/clubs/?is_admin=true`, 'GET', )) as { diff --git a/src/jukebox/player/dto/player-aux-update.dto.ts b/src/jukebox/player/dto/player-aux-update.dto.ts index 4671356..17b3c66 100644 --- a/src/jukebox/player/dto/player-aux-update.dto.ts +++ b/src/jukebox/player/dto/player-aux-update.dto.ts @@ -7,6 +7,7 @@ enum UpdateActionType { PLAYED = 'played', PAUSED = 'paused', CHANGED_TRACKS = 'changed_tracks', + PROGRESS = 'progress', OTHER = 'other', } diff --git a/src/jukebox/player/player.service.ts b/src/jukebox/player/player.service.ts index 8946038..215fb56 100644 --- a/src/jukebox/player/player.service.ts +++ b/src/jukebox/player/player.service.ts @@ -146,8 +146,8 @@ export class PlayerService { * because of Spotify auto play, or because a user manually set the * current track in spotify despite the queue. */ - async setCurrentSpotifyTrack(jukeboxId: number, track: TrackDto): Promise { - return await this.updatePlayerState(jukeboxId, { spotify_track: track }) + async setCurrentSpotifyTrack(jukeboxId: number, track: TrackDto | null): Promise { + return await this.updatePlayerState(jukeboxId, { spotify_track: track ?? undefined }) } /** diff --git a/src/jukebox/player/tests/player.controller.spec.ts b/src/jukebox/player/tests/player.controller.spec.ts index 030a755..418f04f 100644 --- a/src/jukebox/player/tests/player.controller.spec.ts +++ b/src/jukebox/player/tests/player.controller.spec.ts @@ -1,24 +1,25 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' +import { Jukebox } from 'src/jukebox/entities/jukebox.entity' +import { JukeSession } from 'src/jukebox/juke-session/entities/juke-session.entity' +import { JukeSessionMembership } from 'src/jukebox/juke-session/entities/membership.entity' +import { JukeSessionService } from 'src/jukebox/juke-session/juke-session.service' +import { JukeboxService } from 'src/jukebox/jukebox.service' import { QueuedTrack } from 'src/jukebox/queue/entities/queued-track.entity' import { QueueService } from 'src/jukebox/queue/queue.service' +import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { SpotifyService } from 'src/spotify/spotify.service' -import { AxiosProvider, MockCacheProvider } from 'src/utils/mock' +import { MockCacheProvider } from 'src/utils/mock' import { PlayerInteraction } from '../entity/player-interaction.entity' import { PlayerController } from '../player.controller' import { PlayerService } from '../player.service' -import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' -import { JukeSession } from 'src/jukebox/juke-session/entities/juke-session.entity' -import { JukeSessionService } from 'src/jukebox/juke-session/juke-session.service' -import { JukeSessionMembership } from 'src/jukebox/juke-session/entities/membership.entity' -import { NetworkService } from 'src/network/network.service' -import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { JukeboxService } from 'src/jukebox/jukebox.service' -import { Jukebox } from 'src/jukebox/entities/jukebox.entity' describe('PlayerController', () => { let controller: PlayerController @@ -47,7 +48,7 @@ describe('PlayerController', () => { SpotifyService, JukeSessionService, NetworkService, - AxiosProvider, + { provide: HttpService, useValue: {} }, MockCacheProvider, ], }).compile() diff --git a/src/jukebox/player/tests/player.service.spec.ts b/src/jukebox/player/tests/player.service.spec.ts index a75f211..aa8dd47 100644 --- a/src/jukebox/player/tests/player.service.spec.ts +++ b/src/jukebox/player/tests/player.service.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import { CACHE_MANAGER } from '@nestjs/cache-manager' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' @@ -25,11 +26,11 @@ import { SpotifyService } from 'src/spotify/spotify.service' import type { TrackDto } from 'src/track/dto/track.dto' import { Track } from 'src/track/entities/track.entity' import { TrackService } from 'src/track/track.service' -import { MockAxiosProvider, MockCacheProvider, mockUser } from 'src/utils/mock' +import { MockCacheProvider, mockUser } from 'src/utils/mock' +import { DataSource } from 'typeorm' import type { PlayerStateDto } from '../dto' import { InteractionType, PlayerInteraction } from '../entity/player-interaction.entity' import { PlayerService } from '../player.service' -import { DataSource } from 'typeorm' describe('PlayerService', () => { let service: PlayerService @@ -78,7 +79,6 @@ describe('PlayerService', () => { ]), ], providers: [ - MockAxiosProvider, MockCacheProvider, PlayerService, SpotifyService, @@ -90,6 +90,7 @@ describe('PlayerService', () => { AccountLinkService, SpotifyService, SpotifyAuthService, + { provide: HttpService, useValue: {} }, ], }).compile() @@ -102,9 +103,13 @@ describe('PlayerService', () => { cache = module.get(CACHE_MANAGER) jukebox = await jukeboxService.create({ club_id: clubId, name: 'Test Jukebox' }) - jukeSession = await jukeSessionService.create(jukebox.id, { - end_at: new Date(new Date().getTime() + 1000 * 60 * 60 * 2), - }) + jukeSession = await jukeSessionService.create( + jukebox.id, + { + end_at: new Date(new Date().getTime() + 1000 * 60 * 60 * 2), + }, + mockUser.token, + ) jukeSessionMembership = await jukeSessionService.createMembership(jukeSession.id, { user_id: userId, }) diff --git a/src/jukebox/queue/tests/queue.controller.spec.ts b/src/jukebox/queue/tests/queue.controller.spec.ts index 98fb882..faf4598 100644 --- a/src/jukebox/queue/tests/queue.controller.spec.ts +++ b/src/jukebox/queue/tests/queue.controller.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' @@ -19,13 +20,13 @@ import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { SpotifyService } from 'src/spotify/spotify.service' import { Track } from 'src/track/entities/track.entity' import { TrackService } from 'src/track/track.service' -import { MockAxiosProvider, mockSpotifyAccount } from 'src/utils/mock' +import { mockSpotifyAccount, mockUser } from 'src/utils/mock' import { mockCreateTrack } from 'src/utils/mock/mock-create-track' import { mockTrackDetails } from 'src/utils/mock/mock-track-details' +import { DataSource } from 'typeorm' import { QueuedTrack } from '../entities/queued-track.entity' import { QueueController } from '../queue.controller' import { QueueService } from '../queue.service' -import { DataSource } from 'typeorm' describe('QueueController', () => { let module: TestingModule @@ -68,7 +69,7 @@ describe('QueueController', () => { ], controllers: [QueueController], providers: [ - MockAxiosProvider, + { provide: HttpService, useValue: {} }, QueueService, TrackService, JukeSessionService, @@ -89,9 +90,13 @@ describe('QueueController', () => { spotifyAuthService = module.get(SpotifyAuthService) jukebox = await jukeboxService.create({ name: 'Test Jukebox', club_id: 1 }) - jukeSession = await jukeSessionService.create(jukebox.id, { - end_at: new Date(new Date().getTime() + 30 * 60 * 1000), - }) + jukeSession = await jukeSessionService.create( + jukebox.id, + { + end_at: new Date(new Date().getTime() + 30 * 60 * 1000), + }, + mockUser.token, + ) sessionId = jukeSession.id jukeSessionMembership = await jukeSessionService.createMembership(jukeSession.id, { user_id: 1, diff --git a/src/jukebox/queue/tests/queue.service.spec.ts b/src/jukebox/queue/tests/queue.service.spec.ts index 3203199..e88f1c7 100644 --- a/src/jukebox/queue/tests/queue.service.spec.ts +++ b/src/jukebox/queue/tests/queue.service.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' @@ -19,13 +20,13 @@ import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { SpotifyService } from 'src/spotify/spotify.service' import { Track } from 'src/track/entities/track.entity' import { TrackService } from 'src/track/track.service' -import { MockAxiosProvider, mockSpotifyAccount } from 'src/utils/mock' +import { mockSpotifyAccount, mockUser } from 'src/utils/mock' import { mockCreateTrack } from 'src/utils/mock/mock-create-track' import { mockTrackDetails } from 'src/utils/mock/mock-track-details' +import { DataSource } from 'typeorm' import { QueuedTrack } from '../entities/queued-track.entity' import { QueueController } from '../queue.controller' import { QueueService } from '../queue.service' -import { DataSource } from 'typeorm' describe('QueueService', () => { let module: TestingModule @@ -63,7 +64,7 @@ describe('QueueService', () => { ], controllers: [QueueController], providers: [ - MockAxiosProvider, + { provide: HttpService, useValue: {} }, QueueService, TrackService, JukeSessionService, @@ -89,9 +90,13 @@ describe('QueueService', () => { spotifyAuthService = module.get(SpotifyAuthService) jukebox = await jukeboxService.create({ name: 'Test Jukebox', club_id: 1 }) - jukeSession = await jukeSessionService.create(jukebox.id, { - end_at: new Date(new Date().getTime() + 30 * 60 * 1000), - }) + jukeSession = await jukeSessionService.create( + jukebox.id, + { + end_at: new Date(new Date().getTime() + 30 * 60 * 1000), + }, + mockUser.token, + ) sessionId = jukeSession.id jukeSessionMembership = await jukeSessionService.createMembership(sessionId, { user_id: 1, diff --git a/src/jukebox/tests/jukebox.controller.spec.ts b/src/jukebox/tests/jukebox.controller.spec.ts index 4a314c6..27db8a5 100644 --- a/src/jukebox/tests/jukebox.controller.spec.ts +++ b/src/jukebox/tests/jukebox.controller.spec.ts @@ -1,13 +1,14 @@ +import { NotFoundException } from '@nestjs/common' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' +import { NetworkService } from 'src/network/network.service' +import { mockUser } from 'src/utils/mock' +import { DataSource } from 'typeorm' import { Jukebox, TimeFormat } from '../entities/jukebox.entity' import { JukeboxController } from '../jukebox.controller' import { JukeboxService } from '../jukebox.service' -import { NotFoundException } from '@nestjs/common' -import { NetworkService } from 'src/network/network.service' -import { DataSource } from 'typeorm' describe('JukeboxController', () => { let module: TestingModule @@ -21,7 +22,6 @@ describe('JukeboxController', () => { controllers: [JukeboxController], providers: [ JukeboxService, - NetworkService, { provide: NetworkService, useValue: { @@ -81,7 +81,7 @@ describe('JukeboxController', () => { // TODO: ADD TESTING OF AUTO CREATION ON CLUB W/O JUKEBOX it('should find all jukeboxes with a clubId and create if none exist for admin', async () => { - const adminResult = await controller.findAll(adminClubId) + const adminResult = await controller.findAll(adminClubId, mockUser) expect(adminResult.length).toBeGreaterThanOrEqual(1) expect(adminResult[0].club_id).toEqual(adminClubId) @@ -92,12 +92,12 @@ describe('JukeboxController', () => { const name2 = 'FindAll2' const jukebox2 = await controller.create({ name: name2, club_id: clubId1 }) - const result1 = await controller.findAll(clubId1) + const result1 = await controller.findAll(clubId1, mockUser) expect(result1.length).toBeGreaterThanOrEqual(2) expect(result1.some((j) => j.id === jukebox1.id)).toBeTruthy() expect(result1.some((j) => j.id === jukebox2.id)).toBeTruthy() - const result2 = await controller.findAll(123123) + const result2 = await controller.findAll(123123, mockUser) expect(result2.length).toEqual(0) }) diff --git a/src/jukebox/tests/jukebox.service.spec.ts b/src/jukebox/tests/jukebox.service.spec.ts index a59afe7..35af66e 100644 --- a/src/jukebox/tests/jukebox.service.spec.ts +++ b/src/jukebox/tests/jukebox.service.spec.ts @@ -2,11 +2,10 @@ import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' +import { NetworkService } from 'src/network/network.service' +import { Track } from 'src/track/entities/track.entity' import { Jukebox } from '../entities/jukebox.entity' import { JukeboxService } from '../jukebox.service' -import { Track } from 'src/track/entities/track.entity' -import { NetworkService } from 'src/network/network.service' -import { AxiosProvider } from 'src/utils/mock' describe('JukeboxService', () => { let service: JukeboxService @@ -14,7 +13,13 @@ describe('JukeboxService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [DatabaseModule, TypeOrmModule.forFeature([Jukebox, Track])], - providers: [AxiosProvider, JukeboxService, NetworkService], + providers: [ + JukeboxService, + { + provide: NetworkService, + useValue: {}, + }, + ], }).compile() service = module.get(JukeboxService) diff --git a/src/network/network.module.ts b/src/network/network.module.ts index 04ffbcc..8565bd2 100644 --- a/src/network/network.module.ts +++ b/src/network/network.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common' import { AxiosProvider } from 'src/utils/mock/mock-axios-provider' import { NetworkService } from './network.service' +import { HttpModule, HttpService } from '@nestjs/axios' @Module({ - providers: [NetworkService, AxiosProvider], - exports: [NetworkService, AxiosProvider], + imports: [HttpModule], + providers: [NetworkService], + exports: [NetworkService], }) export class NetworkModule {} diff --git a/src/network/network.service.spec.ts b/src/network/network.service.spec.ts index 050fab2..aa15762 100644 --- a/src/network/network.service.spec.ts +++ b/src/network/network.service.spec.ts @@ -1,6 +1,6 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' -import { AxiosProvider } from 'src/utils/mock/mock-axios-provider' import { NetworkService } from './network.service' describe('NetworkService', () => { @@ -8,7 +8,7 @@ describe('NetworkService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [NetworkService, AxiosProvider], + providers: [NetworkService, { provide: HttpService, useValue: {} }], }).compile() service = module.get(NetworkService) diff --git a/src/network/network.service.ts b/src/network/network.service.ts index 53aa8da..055ba96 100644 --- a/src/network/network.service.ts +++ b/src/network/network.service.ts @@ -1,7 +1,8 @@ +import { HttpService } from '@nestjs/axios' import { Injectable } from '@nestjs/common' -import { Axios, AxiosRequestConfig } from 'axios' +import type { AxiosRequestConfig } from 'axios' import { CLUBS_URL, NODE_ENV } from 'src/config' -import { UserDto } from 'src/shared' +import type { UserDto } from 'src/shared' import { sleep } from 'src/utils' @Injectable() @@ -12,9 +13,10 @@ export class NetworkService { } private token = '' - constructor(protected axios: Axios) {} + constructor(protected httpService: HttpService) {} public sendRequest = async ( + token: string, url: string, method?: 'GET' | 'POST' | 'PUT' | 'DELETE', body?: AxiosRequestConfig['data'], @@ -24,11 +26,11 @@ export class NetworkService { await sleep(1000) } - const res = await this.axios.request({ + const res = await this.httpService.axiosRef.request({ method: method || 'GET', url, headers: { - Authorization: `Token ${this.token}`, + Authorization: `Token ${token}`, 'Content-Type': 'application/json', ...config?.headers, @@ -44,16 +46,12 @@ export class NetworkService { } } - setToken(token: string) { - this.token = token - } - isToken(): boolean { return !!this.isToken } - async fetchUser(): Promise { - const res = await this.sendRequest(this.routes.getUser) + async fetchUser(token: string): Promise { + const res = await this.sendRequest(token, this.routes.getUser) if (res.status > 299) { throw new Error('Error fetching data from network') @@ -63,6 +61,7 @@ export class NetworkService { id: +res.data.id, email: res.data.email ?? '', username: res.data.username, + token, } } } diff --git a/src/shared/dtos/user.dto.ts b/src/shared/dtos/user.dto.ts index 07e13bf..3d8852e 100644 --- a/src/shared/dtos/user.dto.ts +++ b/src/shared/dtos/user.dto.ts @@ -9,4 +9,6 @@ export class UserDto { @Expose() username: string + + token: string } diff --git a/src/spotify/spotify-auth.service.ts b/src/spotify/spotify-auth.service.ts index 2b67807..8e0a803 100644 --- a/src/spotify/spotify-auth.service.ts +++ b/src/spotify/spotify-auth.service.ts @@ -13,12 +13,13 @@ import { CreateSpotifyAccountDto, UpdateSpotifyAccountDto } from './dto/spotify- import { SpotifyTokensDto } from './dto/spotify-tokens.dto' import { isSpotifyLink, SpotifyAccount } from './entities/spotify-account.entity' import { SpotifyBaseService } from './spotify-base.service' +import { HttpService } from '@nestjs/axios' @Injectable() export class SpotifyAuthService extends SpotifyBaseService { constructor( @InjectRepository(SpotifyAccount) private repo: Repository, - protected axios: Axios, + protected httpService: HttpService, ) { super() } @@ -31,7 +32,7 @@ export class SpotifyAuthService extends SpotifyBaseService { } const authBuffer = Buffer.from(SPOTIFY_CLIENT_ID + ':' + SPOTIFY_CLIENT_SECRET) - const res = await this.axios + const res = await this.httpService.axiosRef .post('https://accounts.spotify.com/api/token', body, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -116,7 +117,7 @@ export class SpotifyAuthService extends SpotifyBaseService { }) const authBuffer = Buffer.from(SPOTIFY_CLIENT_ID + ':' + SPOTIFY_CLIENT_SECRET) - const res = await this.axios + const res = await this.httpService.axiosRef .post('https://accounts.spotify.com/api/token', body, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/src/spotify/spotify.module.ts b/src/spotify/spotify.module.ts index fe3e4bf..2b5c5cc 100644 --- a/src/spotify/spotify.module.ts +++ b/src/spotify/spotify.module.ts @@ -1,30 +1,24 @@ +import { HttpModule } from '@nestjs/axios' import { forwardRef, Module } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' import { JukeboxModule } from 'src/jukebox/jukebox.module' import { NetworkModule } from 'src/network/network.module' -import { NetworkService } from '../network/network.service' -import { AxiosProvider } from '../utils/mock/mock-axios-provider' import { SpotifyAccount } from './entities/spotify-account.entity' import { SpotifyAuthService } from './spotify-auth.service' import { SpotifyController } from './spotify.controller' import { SpotifyService } from './spotify.service' -import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' @Module({ imports: [ NetworkModule, + HttpModule, TypeOrmModule.forFeature([SpotifyAccount, AccountLink]), forwardRef(() => JukeboxModule), // Prevent circular dependency ], controllers: [SpotifyController], - providers: [ - AxiosProvider, - SpotifyAuthService, - NetworkService, - SpotifyService, - AccountLinkService, - ], + providers: [SpotifyAuthService, SpotifyService, AccountLinkService], exports: [SpotifyAuthService, SpotifyService], }) export class SpotifyModule {} diff --git a/src/spotify/spotify.service.ts b/src/spotify/spotify.service.ts index 706f821..0a2eef0 100644 --- a/src/spotify/spotify.service.ts +++ b/src/spotify/spotify.service.ts @@ -1,9 +1,9 @@ +import { HttpService } from '@nestjs/axios' import { Injectable, Logger } from '@nestjs/common' -import { Axios } from 'axios' +import { MaxInt } from '@spotify/web-api-ts-sdk' import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' import { SpotifyTokensDto } from './dto/spotify-tokens.dto' import { SpotifyBaseService } from './spotify-base.service' -import { MaxInt } from '@spotify/web-api-ts-sdk' export interface ISpotifyService { setPlayerDevice(spotifyAuth: SpotifyTokensDto, deviceId: string): Promise @@ -17,7 +17,7 @@ export interface ISpotifyService { @Injectable() export class SpotifyService extends SpotifyBaseService implements ISpotifyService { - constructor(protected axios: Axios) { + constructor(protected httpService: HttpService) { super() } @@ -36,7 +36,7 @@ export class SpotifyService extends SpotifyBaseService implements ISpotifyServic async queueTrack(spotifyAuth: SpotifyTokensDto, track_uri: string) { // const sdk = this.getSdk(spotifyAuth) // await sdk.player.addItemToPlaybackQueue(track.uri) - await this.axios + await this.httpService.axiosRef .post( `https://api.spotify.com/v1/me/player/queue?uri=${track_uri}`, {}, diff --git a/src/spotify/tests/spotify.controller.spec.ts b/src/spotify/tests/spotify.controller.spec.ts index da61e1e..3905eea 100644 --- a/src/spotify/tests/spotify.controller.spec.ts +++ b/src/spotify/tests/spotify.controller.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' @@ -7,7 +8,6 @@ import { AccountLink } from 'src/jukebox/account-link/entities/account-link.enti import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import { JukeboxService } from 'src/jukebox/jukebox.service' import { NetworkModule } from 'src/network/network.module' -import { MockAxiosProvider } from 'src/utils/mock' import { SpotifyAccount } from '../entities/spotify-account.entity' import { SpotifyAuthService } from '../spotify-auth.service' import { SpotifyController } from '../spotify.controller' @@ -28,8 +28,8 @@ describe('SpotifyController', () => { SpotifyAuthService, SpotifyService, AccountLinkService, - MockAxiosProvider, JukeboxService, + { provide: HttpService, useValue: {} }, ], }).compile() diff --git a/src/spotify/tests/spotify.service.spec.ts b/src/spotify/tests/spotify.service.spec.ts index 4f94d84..679a87c 100644 --- a/src/spotify/tests/spotify.service.spec.ts +++ b/src/spotify/tests/spotify.service.spec.ts @@ -1,8 +1,9 @@ +import { HttpService } from '@nestjs/axios' import { BadRequestException } from '@nestjs/common' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm' -import { Axios } from 'axios' +import type { Axios } from 'axios' import { DatabaseModule } from 'src/config/database.module' import { DataSource, type Repository } from 'typeorm' import { SpotifyAccount } from '../entities/spotify-account.entity' @@ -23,12 +24,7 @@ describe('SpotifyAuthService', () => { module = await Test.createTestingModule({ imports: [DatabaseModule, TypeOrmModule.forFeature([SpotifyAccount])], - providers: [ - SpotifyAuthService, - - // { provide: getRepositoryToken(SpotifyAccount), useClass: Repository }, - { provide: Axios, useValue: axios }, - ], + providers: [SpotifyAuthService, { provide: HttpService, useValue: { axiosRef: axios } }], }).compile() service = module.get(SpotifyAuthService) diff --git a/src/track/tests/track.controller.spec.ts b/src/track/tests/track.controller.spec.ts index 900d757..443ee1f 100644 --- a/src/track/tests/track.controller.spec.ts +++ b/src/track/tests/track.controller.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' @@ -10,7 +11,6 @@ import { NetworkService } from 'src/network/network.service' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { SpotifyService } from 'src/spotify/spotify.service' -import { MockAxiosProvider } from 'src/utils/mock' import { Track } from '../entities/track.entity' import { TrackController } from '../track.controller' import { TrackService } from '../track.service' @@ -26,13 +26,13 @@ describe('TrackController', () => { ], controllers: [TrackController], providers: [ - MockAxiosProvider, TrackService, JukeboxService, NetworkService, AccountLinkService, SpotifyService, SpotifyAuthService, + { provide: HttpService, useValue: {} }, ], }).compile() diff --git a/src/track/tests/track.service.spec.ts b/src/track/tests/track.service.spec.ts index 62502b5..b7588d9 100644 --- a/src/track/tests/track.service.spec.ts +++ b/src/track/tests/track.service.spec.ts @@ -1,3 +1,4 @@ +import { HttpService } from '@nestjs/axios' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' @@ -12,12 +13,12 @@ import { NetworkService } from 'src/network/network.service' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { SpotifyService } from 'src/spotify/spotify.service' -import { MockAxiosProvider, mockSpotifyAccount } from 'src/utils/mock' +import { mockSpotifyAccount } from 'src/utils/mock' import { mockCreateTrack } from 'src/utils/mock/mock-create-track' import { mockTrackDetails } from 'src/utils/mock/mock-track-details' +import { DataSource } from 'typeorm' import { Track } from '../entities/track.entity' import { TrackService } from '../track.service' -import { DataSource } from 'typeorm' describe('TrackService', () => { let module: TestingModule @@ -37,7 +38,6 @@ describe('TrackService', () => { TypeOrmModule.forFeature([Track, Jukebox, AccountLink, SpotifyAccount]), ], providers: [ - MockAxiosProvider, TrackService, JukeboxService, NetworkService, @@ -48,6 +48,10 @@ describe('TrackService', () => { provide: SpotifyService, useValue: { getTrack: jest.fn().mockResolvedValue(mockTrackDetails) }, }, + { + provide: HttpService, + useValue: {}, + }, ], }).compile() diff --git a/src/track/track.module.ts b/src/track/track.module.ts index e2a7c21..d545247 100644 --- a/src/track/track.module.ts +++ b/src/track/track.module.ts @@ -4,27 +4,23 @@ import { AccountLinkService } from 'src/jukebox/account-link/account-link.servic import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import { JukeboxService } from 'src/jukebox/jukebox.service' -import { NetworkService } from 'src/network/network.service' +import { NetworkModule } from 'src/network/network.module' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { SpotifyModule } from 'src/spotify/spotify.module' import { SpotifyService } from 'src/spotify/spotify.service' -import { AxiosProvider } from 'src/utils/mock' import { Track } from './entities/track.entity' import { TrackController } from './track.controller' import { TrackService } from './track.service' @Module({ - imports: [TypeOrmModule.forFeature([Track, AccountLink, Jukebox, SpotifyAccount])], - controllers: [TrackController], - providers: [ - AxiosProvider, - NetworkService, - TrackService, - SpotifyService, - SpotifyAuthService, - AccountLinkService, - JukeboxService, + imports: [ + NetworkModule, + SpotifyModule, + TypeOrmModule.forFeature([Track, AccountLink, Jukebox, SpotifyAccount]), ], + controllers: [TrackController], + providers: [TrackService, AccountLinkService, JukeboxService], exports: [TrackService], }) diff --git a/src/utils/guards/roles.guard.ts b/src/utils/guards/roles.guard.ts index 2396a8a..04cddbc 100644 --- a/src/utils/guards/roles.guard.ts +++ b/src/utils/guards/roles.guard.ts @@ -33,6 +33,7 @@ export class RolesGuard implements CanActivate { const request = context.switchToHttp().getRequest() const { body, query, params } = request let clubId = body?.club_id ?? query?.club_id ?? params?.club_id ?? null + const [_, token] = request.headers.authorization?.split(' ') ?? [] if (clubId === null) { const jukeboxId = params?.jukebox_id ?? query?.jukeboxId ?? null @@ -46,6 +47,7 @@ export class RolesGuard implements CanActivate { } const clubs = (await this.networkService.sendRequest( + token, `${CLUBS_URL}/api/v1/club/clubs/${role === 'admin' ? '?is_admin=true' : ''}`, 'GET', )) as { status: number; description: string; data: { id: number; name: string }[] } diff --git a/src/utils/guards/token.guard.ts b/src/utils/guards/token.guard.ts index 19eead6..b7ea829 100644 --- a/src/utils/guards/token.guard.ts +++ b/src/utils/guards/token.guard.ts @@ -21,7 +21,7 @@ export class TokenGuard implements CanActivate { if (!token) return false - networkService.setToken(token) + // networkService.setToken(token) return true } } diff --git a/src/utils/mock/mock-user.ts b/src/utils/mock/mock-user.ts index a6edb32..3ce4692 100644 --- a/src/utils/mock/mock-user.ts +++ b/src/utils/mock/mock-user.ts @@ -4,4 +4,5 @@ export const mockUser: UserDto = { id: 0, email: 'user@example.com', username: 'JohnDoe52', + token: 'abc123', } From c38b711cd931bf0ec0894f06ebf1494ff7bb0766 Mon Sep 17 00:00:00 2001 From: Isaac Hunter Date: Tue, 30 Dec 2025 01:40:51 -0500 Subject: [PATCH 2/2] fix issues with player sync ws --- docker-compose.network.yml | 2 +- src/jukebox/jukebox.gateway.ts | 10 ++++------ src/jukebox/player/dto/player-aux-update.dto.ts | 2 +- src/track/entities/track.entity.ts | 3 +++ src/track/track.service.ts | 5 ++++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docker-compose.network.yml b/docker-compose.network.yml index 14422fa..f3c93b3 100644 --- a/docker-compose.network.yml +++ b/docker-compose.network.yml @@ -12,7 +12,7 @@ services: - HOST=localhost - LOG_LEVEL=debug - KAFKA_BROKERS=kafka-jbx:9092 - - BASE_URL=http://localhost:8082 + - BASE_URL=${BASE_URL:-http://localhost:8082} - SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID} - SPOTIFY_CLIENT_SECRET=${SPOTIFY_CLIENT_SECRET} - CLUBS_URL=http://clubs-network-proxy:8080 diff --git a/src/jukebox/jukebox.gateway.ts b/src/jukebox/jukebox.gateway.ts index bbe5886..6771997 100644 --- a/src/jukebox/jukebox.gateway.ts +++ b/src/jukebox/jukebox.gateway.ts @@ -140,7 +140,6 @@ export class JukeboxGateway implements OnGatewayInit { @SubscribeMessage('player-join') async handlePlayerJoin(@ConnectedSocket() client: Socket, @MessageBody() payload: PlayerJoinDto) { - console.log('http in join:', this.httpService) if (client['role'] !== 'member' && client['role'] !== 'admin') { throw new WsException('You are not authorized') } @@ -184,6 +183,7 @@ export class JukeboxGateway implements OnGatewayInit { break case 'progress': this.playerService.setCurrentProgress(jukebox_id, progress ?? 0) + break case 'changed_tracks': if (spotify_track && !spotify_track?.spotify_id) { throw new WsException('Track must have a spotify id') @@ -191,10 +191,10 @@ export class JukeboxGateway implements OnGatewayInit { if (!session) { if (!spotify_track) { - this.playerService.setCurrentSpotifyTrack(jukebox_id, null) + // this.playerService.setCurrentSpotifyTrack(jukebox_id, null) } else { const track = await this.tracksService.getTrack(spotify_track?.spotify_id, jukebox_id) - this.playerService.setCurrentSpotifyTrack(jukebox_id, track) + await this.playerService.setCurrentSpotifyTrack(jukebox_id, track) } } else { // Check if next track was next in queue, if so pop it @@ -229,9 +229,7 @@ export class JukeboxGateway implements OnGatewayInit { } const playerState = await this.playerService.getPlayerState(jukebox_id) - const result = { ...playerState, spotify_track: { ...spotify_track, duration_ms } } - console.log(result) - this.server.to(jukebox_id.toString()).emit('player-state-update', result) + this.server.to(jukebox_id.toString()).emit('player-state-update', playerState) } private handleConnectionRejection(client: Socket, loggingMessage: string, errorMessage: string) { diff --git a/src/jukebox/player/dto/player-aux-update.dto.ts b/src/jukebox/player/dto/player-aux-update.dto.ts index 17b3c66..7b1d237 100644 --- a/src/jukebox/player/dto/player-aux-update.dto.ts +++ b/src/jukebox/player/dto/player-aux-update.dto.ts @@ -1,5 +1,5 @@ import { OmitType } from '@nestjs/swagger' -import { Expose, Type } from 'class-transformer' +import { Type } from 'class-transformer' import { IsDate, IsEnum, IsNotEmpty, IsNumber, IsOptional, ValidateNested } from 'class-validator' import { TrackDto } from 'src/track/dto/track.dto' diff --git a/src/track/entities/track.entity.ts b/src/track/entities/track.entity.ts index ea775c0..03117da 100644 --- a/src/track/entities/track.entity.ts +++ b/src/track/entities/track.entity.ts @@ -20,4 +20,7 @@ export class Track extends EntityBase { @Column() spotify_uri: string + + @Column() + duration_ms: number } diff --git a/src/track/track.service.ts b/src/track/track.service.ts index 07dae5c..0ef0760 100644 --- a/src/track/track.service.ts +++ b/src/track/track.service.ts @@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { plainToInstance } from 'class-transformer' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' import { SpotifyService } from 'src/spotify/spotify.service' import { Repository } from 'typeorm' import { CreateTrackDto, TrackDto } from './dto/track.dto' import { Track } from './entities/track.entity' -import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' @Injectable() export class TrackService { @@ -69,6 +69,7 @@ export class TrackService { let track: TrackDto | undefined if (!result) { + console.log(`No result found for ${spotifyId}, creating track...`) if (!jukeboxId) { throw new Error( 'Could not create a track because local track does not yet exist and spotify details could not be found', @@ -80,6 +81,7 @@ export class TrackService { accountLink.spotify_account, spotifyId, ) + console.log('track details:', trackDetails) const releaseYear = new Date(trackDetails.album.release_date).getFullYear() track = await this.create({ release_year: releaseYear, @@ -93,6 +95,7 @@ export class TrackService { preview_url: trackDetails.preview_url, }) } + console.log('track:', track) return plainToInstance(TrackDto, result ?? track) }