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");