From b3ffef9fd9b4bdd41c4f8917bcdb8af40635ffbe Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 6 Oct 2026 19:01:13 +0200 Subject: [PATCH] Use /api/v1 with token authentication All server calls go through a shared axios instance (src/api.js) that gets a token with API_PASSWORD from the runtime config, renews it before it expires and retries once on 401. Character images are loaded with the token as blobs. Removes the unused express and cors dependencies and bumps the version to 1.2.0. Closes #2 --- .gitignore | 3 + README.md | 15 ++++ package-lock.json | 83 ++++++++++++++++------ package.json | 4 +- public/config.template.js | 3 +- src/api.js | 61 ++++++++++++++++ src/components/items/InspirationWindow.vue | 2 +- src/components/items/SearchModal.vue | 4 +- src/components/items/SyncProgressModal.vue | 2 +- src/components/items/characterModal.vue | 38 ++++++++-- src/components/items/extraButtons.vue | 4 +- src/components/layout/TheFooter.vue | 14 ++-- src/main.js | 4 +- 13 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 src/api.js diff --git a/.gitignore b/.gitignore index e896b22..98dcc4d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ pnpm-debug.log* *.njsproj *.sln *.sw? + +# Local runtime config for npm run serve +/public/config.js diff --git a/README.md b/README.md index 7ca823a..de831b7 100644 --- a/README.md +++ b/README.md @@ -20,5 +20,20 @@ npm run build npm run lint ``` +## Configuration +The Docker image writes `config.js` from `public/config.template.js` when the container starts, using these environment variables: + +| Variable | Description | +|---|---| +| `API_HOSTNAME` | URL of the MusicServer, for example `https://music.example.com`. Empty to use the same host as the frontend. | +| `API_PASSWORD` | The MusicServer's `API_PASSWORD`, used to get an API token. Empty if the server has no password. Anyone who can open the frontend can read it. | + +For `npm run serve`, create `public/config.js` (ignored by git): +```js +window.__RUNTIME_CONFIG__ = { API_HOSTNAME: 'http://localhost:8080', API_PASSWORD: '' }; +``` + +When the frontend is served by the MusicServer image, the server provides `config.js` with its own `API_PASSWORD`. + ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/package-lock.json b/package-lock.json index 8573738..e73e63f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,15 @@ { "name": "music-randomizer-vue", - "version": "1.1.2", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "music-randomizer-vue", - "version": "1.1.2", + "version": "1.2.0", "dependencies": { "axios": "^1.7.2", "core-js": "^3.37.1", - "cors": "^2.8.5", - "express": "^4.19.2", "vue": "^3.4.31", "vue-axios": "^3.5.2", "vuex": "^4.1.0" @@ -3185,6 +3183,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -3436,6 +3435,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, "license": "MIT" }, "node_modules/array-union": { @@ -3701,6 +3701,7 @@ "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -3725,6 +3726,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -3734,6 +3736,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/bonjour-service": { @@ -3848,6 +3851,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -3889,6 +3893,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4332,6 +4337,7 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -4344,6 +4350,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4360,6 +4367,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4369,6 +4377,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, "license": "MIT" }, "node_modules/copy-webpack-plugin": { @@ -4447,23 +4456,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4998,6 +4990,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5007,6 +5000,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", @@ -5201,6 +5195,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -5231,6 +5226,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5358,6 +5354,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -5767,6 +5764,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5895,6 +5893,7 @@ "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -5941,6 +5940,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -5950,6 +5950,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -6096,6 +6097,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -6114,6 +6116,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -6123,6 +6126,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/find-cache-dir": { @@ -6232,6 +6236,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6255,6 +6260,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6784,6 +6790,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -6874,6 +6881,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -6969,12 +6977,14 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -7781,6 +7791,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7803,6 +7814,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7839,6 +7851,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7862,6 +7875,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -8107,6 +8121,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8264,6 +8279,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8273,6 +8289,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8323,6 +8340,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -8591,6 +8609,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8648,6 +8667,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, "license": "MIT" }, "node_modules/path-type": { @@ -9515,6 +9535,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -9565,6 +9586,7 @@ "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -9611,6 +9633,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9620,6 +9643,7 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -9990,6 +10014,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -10010,6 +10035,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/sax": { @@ -10076,6 +10102,7 @@ "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -10100,6 +10127,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -10109,6 +10137,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/serialize-javascript": { @@ -10202,6 +10231,7 @@ "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -10235,6 +10265,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, "license": "ISC" }, "node_modules/shallow-clone": { @@ -10290,6 +10321,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10309,6 +10341,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10325,6 +10358,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10343,6 +10377,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10532,6 +10567,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -10969,6 +11005,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -11028,6 +11065,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -11102,6 +11140,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -11166,6 +11205,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -11197,6 +11237,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" diff --git a/package.json b/package.json index b9a9707..36b9428 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "music-randomizer-vue", - "version": "1.1.2", + "version": "1.2.0", "private": true, "scripts": { "serve": "vue-cli-service serve", @@ -10,8 +10,6 @@ "dependencies": { "axios": "^1.7.2", "core-js": "^3.37.1", - "cors": "^2.8.5", - "express": "^4.19.2", "vue": "^3.4.31", "vue-axios": "^3.5.2", "vuex": "^4.1.0" diff --git a/public/config.template.js b/public/config.template.js index 0d796f2..8a0bf7d 100644 --- a/public/config.template.js +++ b/public/config.template.js @@ -1,3 +1,4 @@ window.__RUNTIME_CONFIG__ = { - API_HOSTNAME: '${API_HOSTNAME}' + API_HOSTNAME: '${API_HOSTNAME}', + API_PASSWORD: '${API_PASSWORD}' }; diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..4574277 --- /dev/null +++ b/src/api.js @@ -0,0 +1,61 @@ +import axios from "axios"; + +const config = window.__RUNTIME_CONFIG__ || {}; +const hostname = config.API_HOSTNAME || ""; + +// Request a new token this long before the current one expires +const EXPIRY_MARGIN_MS = 5 * 60 * 1000; + +// Client for the MusicServer /api/v1 endpoints, with token authentication. +export const api = axios.create({ baseURL: `${hostname}/api/v1` }); + +// Client for the server routes outside /api/v1 (the /find search). +export const legacyApi = axios.create({ baseURL: hostname }); + +let token = null; +let expiresAt = 0; +let pendingToken = null; + +function isTokenRequest(request) { + return request.url === "/token"; +} + +// getToken returns a valid token, requesting a new one when needed. +// Concurrent callers share one request, since token creation is rate limited. +function getToken() { + if (token && Date.now() < expiresAt - EXPIRY_MARGIN_MS) { + return Promise.resolve(token); + } + if (!pendingToken) { + pendingToken = api + .post("/token", { password: config.API_PASSWORD || "", client_type: "web" }) + .then((response) => { + token = response.data.token; + expiresAt = new Date(response.data.expires_at).getTime(); + return token; + }) + .finally(() => { + pendingToken = null; + }); + } + return pendingToken; +} + +api.interceptors.request.use(async (request) => { + if (!isTokenRequest(request)) { + request.headers.Authorization = `Bearer ${await getToken()}`; + } + return request; +}); + +// On 401 the token is no longer valid (for example after a server restart): +// get a new one and retry the request once. +api.interceptors.response.use(undefined, (error) => { + const request = error.config; + if (error.response?.status !== 401 || !request || isTokenRequest(request) || request.retried) { + return Promise.reject(error); + } + request.retried = true; + token = null; + return api(request); +}); diff --git a/src/components/items/InspirationWindow.vue b/src/components/items/InspirationWindow.vue index c8bace5..b13c163 100644 --- a/src/components/items/InspirationWindow.vue +++ b/src/components/items/InspirationWindow.vue @@ -92,7 +92,7 @@ export default { reloadSoundtracks() { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/all`, + url: `/music/all`, }) .then((response) => { this.allSoundtracksList = response.data; diff --git a/src/components/items/SearchModal.vue b/src/components/items/SearchModal.vue index bd49e21..ec93d73 100644 --- a/src/components/items/SearchModal.vue +++ b/src/components/items/SearchModal.vue @@ -73,9 +73,9 @@ export default { } this.loading = true; try { - const response = await this.axios({ + const response = await this.legacyAxios({ method: "post", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/find`, + url: "/find", data: new URLSearchParams({ search_term: this.searchTerm }), headers: { "Content-Type": "application/x-www-form-urlencoded", diff --git a/src/components/items/SyncProgressModal.vue b/src/components/items/SyncProgressModal.vue index 652ee8c..54e4f43 100644 --- a/src/components/items/SyncProgressModal.vue +++ b/src/components/items/SyncProgressModal.vue @@ -130,7 +130,7 @@ export default { try { const response = await this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/sync/progress`, + url: "/sync/progress", }); const data = response.data; diff --git a/src/components/items/characterModal.vue b/src/components/items/characterModal.vue index 8d29fd9..f761a1f 100644 --- a/src/components/items/characterModal.vue +++ b/src/components/items/characterModal.vue @@ -38,6 +38,8 @@ export default { show: false, playerName: "", characters: [], + // Object URLs of the character images, by file name + imageUrls: {}, isLoading: false, }; }, @@ -55,12 +57,13 @@ export default { try { const response = await this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/characters`, + url: "/character/list", }); this.characters = response.data || []; + await this.fetchImages(); } catch (error) { console.error("Failed to fetch characters:", error); - // Fallback to hardcoded list if API fails + // Fall back to the characters bundled with the frontend this.characters = [ "Adol.png", "Link.png", @@ -76,24 +79,45 @@ export default { "Raiden.png", "PaperMario.png", ]; + this.imageUrls = Object.fromEntries( + this.characters.map((character) => [character, `characters/${character}`]) + ); } finally { this.isLoading = false; } }, + // Images need the token, which can't send, so they are + // downloaded once and shown through object URLs. + async fetchImages() { + await Promise.all( + this.characters + .filter((character) => !this.imageUrls[character]) + .map(async (character) => { + try { + const response = await this.axios({ + method: "get", + url: "/character", + params: { name: character }, + responseType: "blob", + }); + this.imageUrls[character] = URL.createObjectURL(response.data); + } catch (error) { + console.error(`Failed to fetch character ${character}:`, error); + } + }) + ); + }, getCharacterImageUrl(character) { - return `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/character?name=${encodeURIComponent(character)}`; + return this.imageUrls[character] || "characters/noCharacter.png"; }, getCharacterName(character) { // Remove file extension return character.replace(/\.(png|jpg|jpeg|gif)$/i, ""); }, chooseFighter(characterFilename) { - const apiHostname = window.__RUNTIME_CONFIG__.API_HOSTNAME; - const profile = `${apiHostname}/character?name=${encodeURIComponent(characterFilename)}`; - let payload = { playerName: this.playerName, - profile: profile, + profile: this.getCharacterImageUrl(characterFilename), }; this.$store.dispatch("changePlayerProfile", payload); this.closeModal(); diff --git a/src/components/items/extraButtons.vue b/src/components/items/extraButtons.vue index 330814c..4d7964a 100644 --- a/src/components/items/extraButtons.vue +++ b/src/components/items/extraButtons.vue @@ -97,7 +97,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/reset`, + url: `/music/reset`, }) .then(() => { resolve(); @@ -112,7 +112,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "post", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/sync`, + url: "/sync", }) .then((response) => { resolve(response); diff --git a/src/components/layout/TheFooter.vue b/src/components/layout/TheFooter.vue index 87e3e79..014a7bb 100644 --- a/src/components/layout/TheFooter.vue +++ b/src/components/layout/TheFooter.vue @@ -167,7 +167,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/rand`, + url: `/music/rand`, responseType: "blob", onDownloadProgress: (progressEvent) => { let percentCompleted = Math.round( @@ -197,7 +197,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/rand/low`, + url: `/music/rand/low`, responseType: "blob", onDownloadProgress: (progressEvent) => { let percentCompleted = Math.round( @@ -227,7 +227,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/addPlayed`, + url: `/music/addPlayed`, }) .then(() => { resolve(false); @@ -242,7 +242,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/addQue`, + url: `/music/addQue`, }) .then(() => { resolve(false); @@ -257,7 +257,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/info`, + url: `/music/info`, }) .then((response) => { let soundtrackInfoObject = { @@ -279,7 +279,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music/list`, + url: `/music/list`, }) .then((response) => { let tracklistArray = response.data; @@ -296,7 +296,7 @@ export default { return new Promise((resolve, reject) => { this.axios({ method: "get", - url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/music?song=${trackNumber}`, + url: `/music?song=${trackNumber}`, responseType: "blob", }) .then((response) => { diff --git a/src/main.js b/src/main.js index 3528cf4..6219fa3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,8 +1,8 @@ import { createApp } from "vue"; import { createStore } from "vuex"; import App from "./App.vue"; -import axios from "axios"; import VueAxios from "vue-axios"; +import { api, legacyApi } from "./api"; const store = createStore({ state() { @@ -256,6 +256,6 @@ const store = createStore({ const app = createApp(App); app.use(store); -app.use(VueAxios, axios); +app.use(VueAxios, { axios: api, legacyAxios: legacyApi }); app.mount("#app");