diff --git a/.env.example b/.env.example index 6a1e14c..5057495 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,22 @@ REDIRECT_URI=http://127.0.0.1:5000/callback GENAI_API_KEY=your_gemini_api_key # Flask Configuration +# Required when FLASK_ENV=production; generated per-restart in development FLASK_SECRET_KEY=your_secure_random_key_here FLASK_ENV=development # SERVER_NAME=127.0.0.1:5000 # Optional + +# Dev server (never enable the debugger on a public interface) +FLASK_DEBUG=0 +FLASK_RUN_HOST=127.0.0.1 +FLASK_RUN_PORT=5000 + +# Comma-separated origins allowed to call the API with cookies +# CORS_ORIGINS=http://localhost:8080,http://127.0.0.1:8080 +# Comma-separated origins the OAuth flow may redirect back to (defaults to CORS_ORIGINS) +# ALLOWED_REDIRECT_ORIGINS=http://localhost:8080 +# DEFAULT_FRONTEND_REDIRECT=http://127.0.0.1:8080/preferences + +# Limits +# MAX_PLAYLIST_LENGTH=100 +# MAX_CONTENT_LENGTH=2097152 diff --git a/.gitignore b/.gitignore index da59f65..78a470b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,12 +27,16 @@ ENV/ # Environment Variables .env +.env.* +!.env.example # Flask instance/ .webassets-cache .flask_session/ flask_session/ +flask_session_data/ +*.db # OS .DS_Store diff --git a/UI_Front/package-lock.json b/UI_Front/package-lock.json index dc569a5..1940c36 100644 --- a/UI_Front/package-lock.json +++ b/UI_Front/package-lock.json @@ -912,6 +912,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2312,9 +2329,9 @@ "license": "MIT" }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -2328,9 +2345,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.0.tgz", + "integrity": "sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==", "cpu": [ "arm" ], @@ -2342,9 +2359,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.0.tgz", + "integrity": "sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==", "cpu": [ "arm64" ], @@ -2356,9 +2373,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.0.tgz", + "integrity": "sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==", "cpu": [ "arm64" ], @@ -2370,9 +2387,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.0.tgz", + "integrity": "sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==", "cpu": [ "x64" ], @@ -2383,10 +2400,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.0.tgz", + "integrity": "sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.0.tgz", + "integrity": "sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.0.tgz", + "integrity": "sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==", "cpu": [ "arm" ], @@ -2398,9 +2443,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.0.tgz", + "integrity": "sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==", "cpu": [ "arm" ], @@ -2412,9 +2457,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.0.tgz", + "integrity": "sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==", "cpu": [ "arm64" ], @@ -2426,9 +2471,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.0.tgz", + "integrity": "sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==", "cpu": [ "arm64" ], @@ -2439,10 +2484,52 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.0.tgz", + "integrity": "sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.0.tgz", + "integrity": "sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.0.tgz", + "integrity": "sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.0.tgz", + "integrity": "sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==", "cpu": [ "ppc64" ], @@ -2454,9 +2541,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.0.tgz", + "integrity": "sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.0.tgz", + "integrity": "sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==", "cpu": [ "riscv64" ], @@ -2468,9 +2569,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.0.tgz", + "integrity": "sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==", "cpu": [ "s390x" ], @@ -2482,9 +2583,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.0.tgz", + "integrity": "sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==", "cpu": [ "x64" ], @@ -2496,9 +2597,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.0.tgz", + "integrity": "sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==", "cpu": [ "x64" ], @@ -2509,10 +2610,38 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.0.tgz", + "integrity": "sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.0.tgz", + "integrity": "sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.0.tgz", + "integrity": "sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==", "cpu": [ "arm64" ], @@ -2524,9 +2653,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.0.tgz", + "integrity": "sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==", "cpu": [ "ia32" ], @@ -2537,10 +2666,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.0.tgz", + "integrity": "sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.0.tgz", + "integrity": "sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==", "cpu": [ "x64" ], @@ -2917,9 +3060,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -3242,9 +3385,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -3252,13 +3395,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3359,10 +3502,22 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3492,14 +3647,15 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { @@ -3521,29 +3677,46 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4011,9 +4184,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4202,9 +4375,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4694,16 +4867,16 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -4737,16 +4910,16 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -4875,9 +5048,10 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4907,21 +5081,21 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5000,9 +5174,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5012,40 +5186,52 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, "engines": { - "node": ">= 0.8" + "node": ">= 6" } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -5228,10 +5414,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5320,9 +5516,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.castarray": { @@ -5904,9 +6100,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5943,9 +6139,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -6183,12 +6379,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/picocolors": { @@ -6198,9 +6395,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -6228,9 +6425,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -6247,7 +6444,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6411,10 +6608,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -6427,12 +6627,13 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -6488,18 +6689,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/react": { @@ -6621,12 +6822,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.4" }, "engines": { "node": ">=14.0.0" @@ -6636,13 +6837,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.4", + "react-router": "6.30.6" }, "engines": { "node": ">=14.0.0" @@ -6796,13 +6997,13 @@ } }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.0.tgz", + "integrity": "sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -6812,22 +7013,32 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.0", + "@rollup/rollup-android-arm64": "4.63.0", + "@rollup/rollup-darwin-arm64": "4.63.0", + "@rollup/rollup-darwin-x64": "4.63.0", + "@rollup/rollup-freebsd-arm64": "4.63.0", + "@rollup/rollup-freebsd-x64": "4.63.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.0", + "@rollup/rollup-linux-arm-musleabihf": "4.63.0", + "@rollup/rollup-linux-arm64-gnu": "4.63.0", + "@rollup/rollup-linux-arm64-musl": "4.63.0", + "@rollup/rollup-linux-loong64-gnu": "4.63.0", + "@rollup/rollup-linux-loong64-musl": "4.63.0", + "@rollup/rollup-linux-ppc64-gnu": "4.63.0", + "@rollup/rollup-linux-ppc64-musl": "4.63.0", + "@rollup/rollup-linux-riscv64-gnu": "4.63.0", + "@rollup/rollup-linux-riscv64-musl": "4.63.0", + "@rollup/rollup-linux-s390x-gnu": "4.63.0", + "@rollup/rollup-linux-x64-gnu": "4.63.0", + "@rollup/rollup-linux-x64-musl": "4.63.0", + "@rollup/rollup-openbsd-x64": "4.63.0", + "@rollup/rollup-openharmony-arm64": "4.63.0", + "@rollup/rollup-win32-arm64-msvc": "4.63.0", + "@rollup/rollup-win32-ia32-msvc": "4.63.0", + "@rollup/rollup-win32-x64-gnu": "4.63.0", + "@rollup/rollup-win32-x64-msvc": "4.63.0", "fsevents": "~2.3.2" } }, @@ -6983,14 +7194,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -7002,13 +7213,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "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==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -7405,17 +7616,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { @@ -7616,9 +7844,9 @@ } }, "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -7795,15 +8023,18 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { diff --git a/backend/app.py b/backend/app.py index e3b7e65..6ca3179 100644 --- a/backend/app.py +++ b/backend/app.py @@ -10,10 +10,9 @@ def create_app(): # Initialize Session Session(app) - # Initialize CORS - # Allowing specific origins as per main2.py but simplified - CORS(app, - resources={r"/*": {"origins": ["http://localhost:8080", "http://127.0.0.1:8080", "http://localhost:8081", "http://127.0.0.1:8081"]}}, + # Initialize CORS - credentialed requests are limited to configured origins + CORS(app, + resources={r"/*": {"origins": Config.CORS_ORIGINS}}, supports_credentials=True) # Register Blueprints diff --git a/backend/config.py b/backend/config.py index f577914..061bcf8 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,31 +1,50 @@ import os import secrets from datetime import timedelta +from urllib.parse import urlparse from dotenv import load_dotenv load_dotenv() + +def _split_env_list(name, default): + raw = os.getenv(name) + if not raw: + return default + return [item.strip() for item in raw.split(',') if item.strip()] + + class Config: # Flask Settings - SECRET_KEY = os.getenv('FLASK_SECRET_KEY') or secrets.token_hex(32) + IS_PRODUCTION = os.getenv('FLASK_ENV') == 'production' + + _secret_key = os.getenv('FLASK_SECRET_KEY') + if not _secret_key: + if IS_PRODUCTION: + raise RuntimeError( + "FLASK_SECRET_KEY must be set when FLASK_ENV=production" + ) + # Development only: ephemeral key, sessions do not survive a restart + _secret_key = secrets.token_hex(32) + SECRET_KEY = _secret_key + # Ensure usage of 127.0.0.1 for localhost consistency SERVER_NAME = os.getenv('SERVER_NAME') # Optional, helpful for url_for - + + # Reject oversized request bodies (playlist cover images are capped at 256KB) + MAX_CONTENT_LENGTH = int(os.getenv('MAX_CONTENT_LENGTH', 2 * 1024 * 1024)) + # Session Settings SESSION_TYPE = 'filesystem' SESSION_FILE_DIR = os.path.join(os.getcwd(), 'flask_session') SESSION_PERMANENT = True PERMANENT_SESSION_LIFETIME = timedelta(hours=1) SESSION_USE_SIGNER = True - + # Cookie Settings - Production vs Dev SESSION_COOKIE_NAME = 'spotify_session' SESSION_COOKIE_HTTPONLY = True - - # In production (non-localhost), these should be strict - # We detect if we are running locally based on DEBUG or explicit env var - IS_PRODUCTION = os.getenv('FLASK_ENV') == 'production' - + if IS_PRODUCTION: SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = 'None' # Required for cross-site if frontend/backend are separate domains @@ -36,14 +55,32 @@ class Config: SESSION_COOKIE_SAMESITE = 'Lax' # Better for localhost SESSION_COOKIE_DOMAIN = None + # Origins allowed to call the API with credentials + CORS_ORIGINS = _split_env_list('CORS_ORIGINS', [ + "http://localhost:8080", + "http://127.0.0.1:8080", + "http://localhost:8081", + "http://127.0.0.1:8081", + ]) + + # Origins the OAuth flow is allowed to redirect back to + ALLOWED_REDIRECT_ORIGINS = _split_env_list('ALLOWED_REDIRECT_ORIGINS', CORS_ORIGINS) + DEFAULT_FRONTEND_REDIRECT = os.getenv( + 'DEFAULT_FRONTEND_REDIRECT', 'http://127.0.0.1:8080/preferences' + ) + # Spotify OAuth SPOTIFY_CLIENT_ID = os.getenv("CLIENT_ID") SPOTIFY_CLIENT_SECRET = os.getenv("CLIENT_SECRET") SPOTIFY_REDIRECT_URI = os.getenv("REDIRECT_URI") - + # AI / Gemini GENAI_API_KEY = os.getenv("GENAI_API_KEY") + # Playlist limits (bound AI cost and Spotify request volume) + MAX_PLAYLIST_LENGTH = int(os.getenv('MAX_PLAYLIST_LENGTH', 100)) + DEFAULT_PLAYLIST_LENGTH = 20 + @classmethod def validate(cls): """Ensure critical config exists""" @@ -55,3 +92,16 @@ def validate(cls): if missing: raise ValueError(f"Missing required environment variables: {', '.join(missing)}") + + @classmethod + def safe_redirect(cls, url): + """Return url if it points at an allowed origin, else the default redirect.""" + if not url: + return cls.DEFAULT_FRONTEND_REDIRECT + parsed = urlparse(url) + if parsed.scheme not in ('http', 'https') or not parsed.netloc: + return cls.DEFAULT_FRONTEND_REDIRECT + origin = f"{parsed.scheme}://{parsed.netloc}" + if origin in cls.ALLOWED_REDIRECT_ORIGINS: + return url + return cls.DEFAULT_FRONTEND_REDIRECT diff --git a/backend/routes/auth.py b/backend/routes/auth.py index ec507cb..353312c 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -1,12 +1,47 @@ from flask import Blueprint, request, session, redirect, jsonify, current_app from ..config import Config from ..services.spotify import SpotifyService +import logging import secrets -import time from datetime import datetime +logger = logging.getLogger(__name__) + auth_bp = Blueprint('auth', __name__) + +def get_valid_access_token(): + """Return a non-expired Spotify access token, refreshing it if needed. + + Returns None when the caller is not authenticated; the session is cleared in + that case so stale credentials are not reused. + """ + if 'access_token' not in session: + return None + + if datetime.now().timestamp() <= session.get('expires_at', 0): + return session['access_token'] + + refresh = session.get('refresh_token') + if not refresh: + session.clear() + return None + + try: + spotify = SpotifyService(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET) + new_tokens = spotify.refresh_token(refresh) + except Exception: + logger.exception("Spotify token refresh failed") + session.clear() + return None + + session['access_token'] = new_tokens.get('access_token') + if 'refresh_token' in new_tokens: + session['refresh_token'] = new_tokens.get('refresh_token') + session['expires_at'] = datetime.now().timestamp() + new_tokens.get('expires_in', 3600) + session.modified = True + return session['access_token'] + @auth_bp.route('/login') def login(): # Verify config @@ -21,12 +56,9 @@ def login(): state = secrets.token_urlsafe(16) session['oauth_state'] = state - # Store where to go after login (defaulting to frontend) - # In main2.py this was hardcoded or parameter driven. - # Adapting to safe defaults. - # If the user is on localhost:8080 (dev), we redirect there. - frontend_url = request.args.get('redirect', 'http://127.0.0.1:8080/preferences') - session['frontend_redirect'] = frontend_url + # Store where to go after login. Only allow-listed origins are accepted so + # the OAuth flow cannot be turned into an open redirect. + session['frontend_redirect'] = Config.safe_redirect(request.args.get('redirect')) # Force session save session.modified = True @@ -56,7 +88,7 @@ def callback(): stored_state = session.get('oauth_state') received_state = request.args.get('state') - if not stored_state or stored_state != received_state: + if not stored_state or not received_state or not secrets.compare_digest(stored_state, received_state): return jsonify({"error": "Invalid state parameter", "details": "Session may have expired or cross-site request detected."}), 400 error = request.args.get('error') @@ -71,8 +103,9 @@ def callback(): spotify = SpotifyService(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET) try: token_info = spotify.exchange_code_for_token(code, Config.SPOTIFY_REDIRECT_URI) - except Exception as e: - return jsonify({"error": "Token exchange failed", "details": str(e)}), 500 + except Exception: + logger.exception("Spotify token exchange failed") + return jsonify({"error": "Token exchange failed"}), 502 # Store Session session['access_token'] = token_info.get('access_token') @@ -85,44 +118,21 @@ def callback(): profile = spotify.get_user_profile(session['access_token']) session['spotify_user_id'] = profile.get('id') session['spotify_display_name'] = profile.get('display_name') - except Exception as e: - print(f"Warning: Failed to fetch profile on login: {e}") - + except Exception: + logger.exception("Failed to fetch Spotify profile on login") + + # The state is single use + session.pop('oauth_state', None) session.modified = True - - # Redirect back to frontend - redirect_url = session.get('frontend_redirect', 'http://127.0.0.1:8080/') - return redirect(redirect_url) + + # Redirect back to frontend (re-validated in case the stored value is stale) + return redirect(Config.safe_redirect(session.pop('frontend_redirect', None))) @auth_bp.route('/auth/status') def auth_status(): - if 'access_token' not in session: + if not get_valid_access_token(): return jsonify({"authenticated": False}), 401 - - # Check expiry - expires_at = session.get('expires_at', 0) - if datetime.now().timestamp() > expires_at: - # Try refresh - if 'refresh_token' in session: - try: - spotify = SpotifyService(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET) - new_tokens = spotify.refresh_token(session['refresh_token']) - - session['access_token'] = new_tokens.get('access_token') - # Refresh token might not always be returned in a refresh flow, keep old one if so - if 'refresh_token' in new_tokens: - session['refresh_token'] = new_tokens.get('refresh_token') - - session['expires_at'] = datetime.now().timestamp() + new_tokens.get('expires_in', 3600) - session.modified = True - except Exception as e: - print(f"Refresh failed: {e}") - session.clear() - return jsonify({"authenticated": False}), 401 - else: - session.clear() - return jsonify({"authenticated": False}), 401 - + return jsonify({ "authenticated": True, "user": session.get('spotify_display_name'), diff --git a/backend/routes/playlist.py b/backend/routes/playlist.py index 778bf3b..7d64142 100644 --- a/backend/routes/playlist.py +++ b/backend/routes/playlist.py @@ -1,38 +1,94 @@ from flask import Blueprint, request, session, jsonify +import base64 +import binascii import concurrent.futures +import logging +import re +import requests from ..config import Config from ..services.spotify import SpotifyService from ..services.ai import AIService +from .auth import get_valid_access_token + +logger = logging.getLogger(__name__) playlist_bp = Blueprint('playlist', __name__) +# Spotify accepts at most 100 track URIs per request +MAX_TRACK_URIS = 100 +MAX_NAME_LENGTH = 100 +MAX_DESCRIPTION_LENGTH = 300 +MAX_QUERY_LENGTH = 200 +# Spotify rejects cover images larger than 256KB of base64 payload +MAX_COVER_IMAGE_BYTES = 256 * 1024 + +TRACK_URI_RE = re.compile(r'^spotify:track:[A-Za-z0-9]{22}$') + + +def _clamp_playlist_length(value): + if isinstance(value, list): + value = value[0] if value else None + try: + length = int(value) + except (TypeError, ValueError): + return Config.DEFAULT_PLAYLIST_LENGTH + return max(1, min(length, Config.MAX_PLAYLIST_LENGTH)) + + +def _validate_track_uris(uris): + """Return (uris, error). Only well-formed Spotify track URIs are accepted.""" + if not isinstance(uris, list) or not uris: + return None, "No tracks provided" + if len(uris) > MAX_TRACK_URIS: + return None, f"At most {MAX_TRACK_URIS} tracks can be added at once" + for uri in uris: + if not isinstance(uri, str) or not TRACK_URI_RE.match(uri): + return None, "Invalid track URI" + return uris, None + + +def _validate_cover_image(image): + """Return (image, error) for an optional base64 (or data URL) cover image.""" + if image is None: + return None, None + if not isinstance(image, str): + return None, "Invalid image" + payload = image.split(",", 1)[1] if "," in image else image + payload = payload.strip() + try: + decoded = base64.b64decode(payload, validate=True) + except (binascii.Error, ValueError): + return None, "Image must be base64 encoded JPEG data" + if len(payload) > MAX_COVER_IMAGE_BYTES or not decoded: + return None, "Image exceeds the 256KB limit" + return payload, None + + +def _clean_text(value, default, max_length): + if not isinstance(value, str) or not value.strip(): + return default + return value.strip()[:max_length] + + @playlist_bp.route('/Playlist_Generator', methods=['POST']) @playlist_bp.route('/Generate_Preview', methods=['POST']) def generate_preview(): # 1. Auth Check - if 'access_token' not in session: + access_token = get_valid_access_token() + if not access_token: return jsonify({"error": "Not authenticated", "redirect": "/login"}), 401 - + # 2. Get Params - data = request.get_json() or {} + data = request.get_json(silent=True) or {} preferences = data.get('preferences') - if not preferences: + if not isinstance(preferences, dict) or not preferences: return jsonify({"error": "No preferences provided"}), 400 - - playlist_length = preferences.get("playlistLength", 20) - # Handle if frontend sends a list (legacy support) - if isinstance(playlist_length, list): - playlist_length = playlist_length[0] - - try: - playlist_length = int(playlist_length) - except: - playlist_length = 20 + + playlist_length = _clamp_playlist_length(preferences.get("playlistLength")) # 3. AI Generation # Request extra songs to buffer against those not found on Spotify - # Increase buffer to 100% of requested length or at least 10 songs to be safe - buffer_count = max(10, int(playlist_length * 1.0)) + buffer_count = max(10, playlist_length) target_ai_count = playlist_length + buffer_count ai_service = AIService() @@ -41,16 +97,16 @@ def generate_preview(): try: # Generate raw song list with buffer ai_songs = ai_service.generate_playlist_params(preferences, count=target_ai_count) - except Exception as e: - return jsonify({"error": "AI Generation failed", "details": str(e)}), 500 + except Exception: + logger.exception("AI playlist generation failed") + return jsonify({"error": "AI Generation failed"}), 502 # 4. Spotify Search (Parallelized) found_tracks = [] - - # Extract token from session in the main thread - access_token = session['access_token'] - + def search_worker(token, song_info): + if not isinstance(song_info, dict): + return None return spotify_service.search_track( token, song_info.get('name'), @@ -67,8 +123,8 @@ def search_worker(token, song_info): track = future.result() if track: found_tracks.append(track) - except Exception as e: - print(f"Search error: {e}") + except Exception: + logger.exception("Spotify track search failed") if not found_tracks: return jsonify({"error": "No songs found on Spotify matching the criteria"}), 404 @@ -103,17 +159,21 @@ def search_worker(token, song_info): @playlist_bp.route('/Create_Playlist', methods=['POST']) def create_playlist(): - if 'access_token' not in session: + access_token = get_valid_access_token() + if not access_token: return jsonify({"error": "Not authenticated", "redirect": "/login"}), 401 - data = request.get_json() or {} - name = data.get('name', 'AI Generated Bundle') - description = data.get('description', 'Generated by PlaylistAI') - uris = data.get('uris', []) - image = data.get('image') # Base64 string + data = request.get_json(silent=True) or {} + name = _clean_text(data.get('name'), 'AI Generated Bundle', MAX_NAME_LENGTH) + description = _clean_text(data.get('description'), 'Generated by PlaylistAI', MAX_DESCRIPTION_LENGTH) + + uris, error = _validate_track_uris(data.get('uris')) + if error: + return jsonify({"error": error}), 400 - if not uris: - return jsonify({"error": "No tracks provided"}), 400 + image, error = _validate_cover_image(data.get('image')) + if error: + return jsonify({"error": error}), 400 spotify_service = SpotifyService(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET) @@ -121,12 +181,12 @@ def create_playlist(): user_id = session.get('spotify_user_id') if not user_id: # Try to fetch if missing - profile = spotify_service.get_user_profile(session['access_token']) + profile = spotify_service.get_user_profile(access_token) user_id = profile['id'] session['spotify_user_id'] = user_id playlist = spotify_service.create_playlist( - session['access_token'], + access_token, user_id, name=name, description=description, @@ -135,7 +195,7 @@ def create_playlist(): # Add Tracks spotify_service.add_tracks_to_playlist( - session['access_token'], + access_token, playlist['id'], uris ) @@ -143,46 +203,38 @@ def create_playlist(): # Upload Image if provided if image: try: - # Add slight delay to ensure playlist is ready? - # Spotify API usually handles it fine, but sometimes it takes a moment. spotify_service.upload_playlist_cover( - session['access_token'], + access_token, playlist['id'], image ) - except Exception as img_err: - print(f"Failed to upload image: {img_err}") + except Exception: # Don't fail the whole request, just log it + logger.exception("Failed to upload playlist cover image") return jsonify({ "playlist_id": playlist['id'], "message": "Playlist created successfully" }) - except Exception as e: - print(f"Playlist creation failed: {e}") - return jsonify({"error": "Failed to create playlist on Spotify", "details": str(e)}), 500 + except Exception: + logger.exception("Playlist creation failed") + return jsonify({"error": "Failed to create playlist on Spotify"}), 502 @playlist_bp.route('/Search_Track', methods=['GET']) def search_spotify_track(): - if 'access_token' not in session: + access_token = get_valid_access_token() + if not access_token: return jsonify({"error": "Not authenticated"}), 401 - query = request.args.get('q') + query = request.args.get('q', '').strip() if not query: return jsonify({"error": "Missing query"}), 400 + query = query[:MAX_QUERY_LENGTH] spotify_service = SpotifyService(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET) - - # We'll use a direct search here, reusing search_track logic or calling raw search - # Since search_track in service is specific to track/artist, let's just do a raw search here or add a general search to service. - # For now, let's use the service's existing base URL but we need a general search. - # Actually, let's just use requests here for simplicity or extend service. - # Extending service is cleaner. - + try: - # Reuse search_track logic but treat query as song name and ignore artist if not provided? - # Or better, just call Spotify Search API directly here for flexible search params = { "q": query, "type": "track", @@ -192,12 +244,13 @@ def search_spotify_track(): response = requests.get( f"{SpotifyService.BASE_URL}/search", - headers=spotify_service.get_auth_headers(session['access_token']), - params=params + headers=spotify_service.get_auth_headers(access_token), + params=params, + timeout=10 ) if response.status_code != 200: - return jsonify({"error": "Spotify search failed"}), response.status_code + return jsonify({"error": "Spotify search failed"}), 502 data = response.json() tracks = data.get('tracks', {}).get('items', []) @@ -221,26 +274,28 @@ def search_spotify_track(): return jsonify(results) - except Exception as e: - return jsonify({"error": str(e)}), 500 + except Exception: + logger.exception("Spotify track search failed") + return jsonify({"error": "Spotify search failed"}), 502 @playlist_bp.route('/Get_Playlists', methods=['GET']) def get_playlists(): - if 'access_token' not in session: + access_token = get_valid_access_token() + if not access_token: return jsonify({"error": "Not authenticated"}), 401 - + spotify_service = SpotifyService(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET) try: - # Simplified for now, just getting user's playlists - # Logic from main2.py could be adapted if specific playlist fetching is needed - # But this route seemed generic in main2.py - params = {"limit": 50} response = requests.get( f"{SpotifyService.BASE_URL}/me/playlists", - headers=spotify_service.get_auth_headers(session['access_token']), - params=params + headers=spotify_service.get_auth_headers(access_token), + params=params, + timeout=10 ) + if response.status_code != 200: + return jsonify({"error": "Failed to fetch playlists"}), 502 return jsonify(response.json()) - except Exception as e: - return jsonify({"error": str(e)}), 500 + except Exception: + logger.exception("Failed to fetch user playlists") + return jsonify({"error": "Failed to fetch playlists"}), 502 diff --git a/backend/services/spotify.py b/backend/services/spotify.py index 5c6f496..8ca11eb 100644 --- a/backend/services/spotify.py +++ b/backend/services/spotify.py @@ -29,7 +29,7 @@ def exchange_code_for_token(self, code, redirect_uri): 'grant_type': "authorization_code" } - response = requests.post(self.AUTH_URL, data=data, headers=headers) + response = requests.post(self.AUTH_URL, data=data, headers=headers, timeout=10) if response.status_code != 200: raise Exception(f"Token exchange failed: {response.text}") @@ -46,7 +46,7 @@ def refresh_token(self, refresh_token): 'refresh_token': refresh_token } - response = requests.post(self.AUTH_URL, data=data, headers=headers) + response = requests.post(self.AUTH_URL, data=data, headers=headers, timeout=10) if response.status_code != 200: raise Exception(f"Token refresh failed: {response.text}") @@ -122,7 +122,8 @@ def create_playlist(self, access_token, user_id, name, description="Generated by response = requests.post( url, headers=self.get_auth_headers(access_token), - json=data + json=data, + timeout=10 ) response.raise_for_status() return response.json() @@ -139,7 +140,8 @@ def add_tracks_to_playlist(self, access_token, playlist_id, uris): response = requests.post( url, headers=self.get_auth_headers(access_token), - json=data + json=data, + timeout=10 ) response.raise_for_status() return response.json() @@ -147,7 +149,8 @@ def add_tracks_to_playlist(self, access_token, playlist_id, uris): def get_user_profile(self, access_token): response = requests.get( f"{self.BASE_URL}/me", - headers=self.get_auth_headers(access_token) + headers=self.get_auth_headers(access_token), + timeout=10 ) if response.status_code != 200: raise Exception(f"Failed to fetch profile: {response.text}") @@ -175,7 +178,8 @@ def upload_playlist_cover(self, access_token, playlist_id, image_b64): response = requests.put( url, headers=headers, - data=image_b64 + data=image_b64, + timeout=15 ) if response.status_code == 202: diff --git a/flask_session/15c846f2599d1bfc08cc5cb4a5ed974f b/flask_session/15c846f2599d1bfc08cc5cb4a5ed974f deleted file mode 100644 index d4ef5eb..0000000 Binary files a/flask_session/15c846f2599d1bfc08cc5cb4a5ed974f and /dev/null differ diff --git a/flask_session/2029240f6d1128be89ddc32729463129 b/flask_session/2029240f6d1128be89ddc32729463129 deleted file mode 100644 index 264f242..0000000 Binary files a/flask_session/2029240f6d1128be89ddc32729463129 and /dev/null differ diff --git a/flask_session/2f9df8e9888f06aaccaaecb1d59a4fc9 b/flask_session/2f9df8e9888f06aaccaaecb1d59a4fc9 deleted file mode 100644 index 4ec32e6..0000000 Binary files a/flask_session/2f9df8e9888f06aaccaaecb1d59a4fc9 and /dev/null differ diff --git a/flask_session/6676a3c21dc87cff4d3ba176913692a8 b/flask_session/6676a3c21dc87cff4d3ba176913692a8 deleted file mode 100644 index ef71f97..0000000 Binary files a/flask_session/6676a3c21dc87cff4d3ba176913692a8 and /dev/null differ diff --git a/flask_session/7de69e160ce0c51fb5bebb75cab947f7 b/flask_session/7de69e160ce0c51fb5bebb75cab947f7 deleted file mode 100644 index cff64d3..0000000 Binary files a/flask_session/7de69e160ce0c51fb5bebb75cab947f7 and /dev/null differ diff --git a/flask_session/95ccee819a22c21d5f80e3ac3bf8d9f5 b/flask_session/95ccee819a22c21d5f80e3ac3bf8d9f5 deleted file mode 100644 index 20f37c0..0000000 Binary files a/flask_session/95ccee819a22c21d5f80e3ac3bf8d9f5 and /dev/null differ diff --git a/flask_session/b05bbcd9f8e51d0ae8a2761b0fd4223b b/flask_session/b05bbcd9f8e51d0ae8a2761b0fd4223b deleted file mode 100644 index f221b5f..0000000 Binary files a/flask_session/b05bbcd9f8e51d0ae8a2761b0fd4223b and /dev/null differ diff --git a/flask_session/c69ed4b3546c78bcaec840d96af53d56 b/flask_session/c69ed4b3546c78bcaec840d96af53d56 deleted file mode 100644 index 9255e6f..0000000 Binary files a/flask_session/c69ed4b3546c78bcaec840d96af53d56 and /dev/null differ diff --git a/flask_session/c847eff44ad4c3adc9dd06d36b2a59f8 b/flask_session/c847eff44ad4c3adc9dd06d36b2a59f8 deleted file mode 100644 index 36f5c7c..0000000 Binary files a/flask_session/c847eff44ad4c3adc9dd06d36b2a59f8 and /dev/null differ diff --git a/flask_session/cb0ea92ad17f189ecb4adfa207037b12 b/flask_session/cb0ea92ad17f189ecb4adfa207037b12 deleted file mode 100644 index 2e0b6c2..0000000 Binary files a/flask_session/cb0ea92ad17f189ecb4adfa207037b12 and /dev/null differ diff --git a/flask_session/d938c4aed87c8559a4a5a34bf8add27c b/flask_session/d938c4aed87c8559a4a5a34bf8add27c deleted file mode 100644 index 13637c4..0000000 Binary files a/flask_session/d938c4aed87c8559a4a5a34bf8add27c and /dev/null differ diff --git a/flask_session/e8bb3a807506a24b00dc81dd75f79cb9 b/flask_session/e8bb3a807506a24b00dc81dd75f79cb9 deleted file mode 100644 index 50db2b5..0000000 Binary files a/flask_session/e8bb3a807506a24b00dc81dd75f79cb9 and /dev/null differ diff --git a/flask_session/e8e8f8ae24bf1bdbc8be2d885732bcee b/flask_session/e8e8f8ae24bf1bdbc8be2d885732bcee deleted file mode 100644 index 22155a2..0000000 Binary files a/flask_session/e8e8f8ae24bf1bdbc8be2d885732bcee and /dev/null differ diff --git a/flask_session_data/2029240f6d1128be89ddc32729463129 b/flask_session_data/2029240f6d1128be89ddc32729463129 deleted file mode 100644 index 8b04914..0000000 Binary files a/flask_session_data/2029240f6d1128be89ddc32729463129 and /dev/null differ diff --git a/flask_session_data/726fff0f600a0b6e5f8b7fd5fb61a681 b/flask_session_data/726fff0f600a0b6e5f8b7fd5fb61a681 deleted file mode 100644 index 251018a..0000000 Binary files a/flask_session_data/726fff0f600a0b6e5f8b7fd5fb61a681 and /dev/null differ diff --git a/readme.md b/readme.md index 66c91f8..0035849 100644 --- a/readme.md +++ b/readme.md @@ -56,7 +56,9 @@ Spotify_Project/ ```bash python run.py ``` - The server listens on `http://0.0.0.0:5000/` and stores sessions under `flask_session/` by default. + The server listens on `http://127.0.0.1:5000/` and stores sessions under `flask_session/` by default. + Host, port, and the Werkzeug debugger are controlled by `FLASK_RUN_HOST`, `FLASK_RUN_PORT`, and + `FLASK_DEBUG` (the debugger allows remote code execution, so it is off by default). ## Frontend Setup 1. **Install dependencies** diff --git a/requirements.txt b/requirements.txt index 9e69386..25b9e9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ pydantic_core==2.27.2 pyparsing==3.2.1 python-dotenv==1.0.1 redis==5.2.1 -requests==2.32.3 +requests==2.32.5 rsa==4.9 sniffio==1.3.1 spotipy==2.24.0 @@ -48,6 +48,6 @@ tenacity==9.1.2 tqdm==4.67.1 typing_extensions==4.12.2 uritemplate==4.1.1 -urllib3==2.3.0 +urllib3==2.5.0 websockets==15.0.1 Werkzeug==3.1.3 diff --git a/run.py b/run.py index ec0455c..148c027 100644 --- a/run.py +++ b/run.py @@ -6,5 +6,9 @@ if __name__ == "__main__": # Ensure session dir exists os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True) - # Run - app.run(host='0.0.0.0', port=5000, debug=True) + # The Werkzeug debugger allows arbitrary code execution, so it stays off + # unless explicitly enabled for local development. + debug = os.getenv('FLASK_DEBUG', '0').lower() in ('1', 'true', 'yes') + host = os.getenv('FLASK_RUN_HOST', '127.0.0.1') + port = int(os.getenv('FLASK_RUN_PORT', 5000)) + app.run(host=host, port=port, debug=debug)