diff --git a/.env.example b/.env.example index a303c20..f50895e 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ SUPABASE_URL="https://yourhost.supabase.co" SUPABASE_KEY="your-secret-key" +SUPABASE_SERVICE_KEY="your-service-key" +SPOTIFY_CLIENT_ID="your-client-id" +SPOTIFY_CLIENT_SECRET="your-client-secret" \ No newline at end of file diff --git a/DB/playlists.sql b/DB/playlists.sql new file mode 100644 index 0000000..9c39e60 --- /dev/null +++ b/DB/playlists.sql @@ -0,0 +1,17 @@ +create table playlists +( + id text not null primary key, + "spotifyId" text not null, + name text not null, + cover text, + enabled boolean default false not null +); + +CREATE UNIQUE INDEX enabled_playlist_unique_name ON playlists (name) WHERE enabled = true; + +create table categories +( + name text not null, + "playlistId" text not null references playlists, + primary key (name, "playlistId") +); diff --git a/README.md b/README.md index eacb11b..6907cea 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,7 @@ Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introdu Make sure to install the dependencies: ```bash -# npm npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install ``` ## Development Server @@ -25,36 +15,15 @@ bun install Start the development server on `http://localhost:3000`: ```bash -# npm npm run dev - -# pnpm -pnpm run dev - -# yarn -yarn dev - -# bun -bun run dev ``` -## Production - -Build the application for production: - -```bash -# npm -npm run build -``` - -Locally preview production build: - +## TSDocs ```bash -# npm -npm run preview +npm run docs:serve ``` Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. ## Required Environment Variables -Check `.env.demo` for the required environment variables. \ No newline at end of file +Check `.env.example` for the required environment variables. \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index 7ff5887..debde16 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -13,9 +13,15 @@ export default defineNuxtConfig({ }, cookieOptions: { secure: false, //TODO: set to true when deploying + }, + clientOptions: { + db: { + schema: 'beatbuzzer', + } } }, devServer: { host: '0.0.0.0' - } + }, + ssr: false }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b80a5ec..decf1e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,14 @@ "@nuxtjs/tailwindcss": "^6.12.1", "nuxt": "^3.13.0", "vue": "latest", - "vue-router": "latest" + "vue-router": "latest", + "zod": "^3.23.8" }, "devDependencies": { "@nuxt/eslint": "^0.5.7", - "eslint": "^9.11.1" + "@types/node": "^22.8.5", + "eslint": "^9.11.1", + "typedoc": "^0.26.10" } }, "node_modules/@alloc/quick-lru": { @@ -3434,6 +3437,62 @@ "win32" ] }, + "node_modules/@shikijs/core": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.2.tgz", + "integrity": "sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.22.2", + "@shikijs/engine-oniguruma": "1.22.2", + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.2.tgz", + "integrity": "sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-js": "0.4.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.2.tgz", + "integrity": "sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0" + } + }, + "node_modules/@shikijs/types": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.2.tgz", + "integrity": "sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -3561,6 +3620,16 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-proxy": { "version": "1.17.15", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", @@ -3577,13 +3646,23 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", - "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", + "version": "22.8.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz", + "integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/normalize-package-data": { @@ -3604,6 +3683,13 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -3831,6 +3917,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@unhead/dom": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.6.tgz", @@ -5127,6 +5220,17 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5150,6 +5254,28 @@ "node": ">=0.8.0" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5419,6 +5545,17 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -6027,6 +6164,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destr": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", @@ -6058,6 +6205,20 @@ "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -7581,6 +7742,44 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -7606,6 +7805,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", @@ -8590,6 +8800,16 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/listhen": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.7.2.tgz", @@ -8708,6 +8928,13 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -8764,12 +8991,59 @@ "semver": "bin/semver.js" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -8803,6 +9077,100 @@ "node": ">= 0.6" } }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -10018,6 +10386,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex": "^4.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/only": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", @@ -11203,6 +11584,17 @@ "node": ">= 6" } }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/protocols": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", @@ -11230,6 +11622,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11513,6 +11915,13 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "dev": true, + "license": "MIT" + }, "node_modules/regexp-ast-analysis": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", @@ -12188,6 +12597,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shiki": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.2.tgz", + "integrity": "sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.22.2", + "@shikijs/engine-javascript": "1.22.2", + "@shikijs/engine-oniguruma": "1.22.2", + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -12355,6 +12779,17 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -12491,6 +12926,21 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -13118,6 +13568,17 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -13204,6 +13665,45 @@ "node": ">= 0.6" } }, + "node_modules/typedoc": { + "version": "0.26.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.10.tgz", + "integrity": "sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.16.2", + "yaml": "^2.5.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", @@ -13219,6 +13719,13 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ufo": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", @@ -13340,6 +13847,79 @@ "unplugin": "^1.14.1" } }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -13615,6 +14195,36 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "5.4.7", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", @@ -14887,6 +15497,26 @@ "engines": { "node": ">= 14" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index dcf7fd2..ffd3bf5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare" + "postinstall": "nuxt prepare", + "docs": "typedoc", + "docs:serve": "typedoc && npx serve docs" }, "dependencies": { "@hebilicious/authjs-nuxt": "^0.3.5", @@ -16,10 +18,13 @@ "@nuxtjs/tailwindcss": "^6.12.1", "nuxt": "^3.13.0", "vue": "latest", - "vue-router": "latest" + "vue-router": "latest", + "zod": "^3.23.8" }, "devDependencies": { "@nuxt/eslint": "^0.5.7", - "eslint": "^9.11.1" + "@types/node": "^22.8.5", + "eslint": "^9.11.1", + "typedoc": "^0.26.10" } } diff --git a/server/api/v1/README.md b/server/api/v1/README.md new file mode 100644 index 0000000..36bd42f --- /dev/null +++ b/server/api/v1/README.md @@ -0,0 +1,80 @@ +# Examples how to create api handlers + +## Example 1: Simple handler + +Route based: `server/api/v1/hello.ts` will hear on `/api/v1/hello` + +```ts +import {serverSupabaseClient} from '#supabase/server' + +export default defineEventHandler(async (event) => { + const client = await serverSupabaseClient(event) + const {data} = await client.from('calculations').select('*') + + return {data: data} +}) +``` + +## Example 2: Handler with Route parameters +File: `server/api/hello/[name].ts`. +Square brackets indicate wildcard that will get passed to handler. + + +```ts +export default defineEventHandler((event) => { + const name = getRouterParam(event, 'name') + + return `Hello, ${name}!` +}) +``` + +## Example 3: Handler with Query parameters +Example: `/api/query?foo=bar&baz=qux` + +```ts +export default defineEventHandler((event) => { + const query = getQuery(event) + + return { a: query.foo, b: query.baz } +}) +``` + +## Example 4: Handler with POST body + +File: `server/api/v1/hello.POST.ts` +```ts +import { z } from 'zod' + +const userSchema = z.object({ + name: z.string().default('Guest'), + email: z.string().email(), +}) + +export default defineEventHandler(async (event) => { + const result = await readValidatedBody(event, body => userSchema.safeParse(body)) // or `.parse` to directly throw an error + + if (!result.success) + throw result.error.issues + + // User object is validated and typed! + return result.data +}) +``` + +## Example 5: Custom Error handling + +```ts +export default defineEventHandler(async (event) => { + throw createError({ + statusCode: 400, + statusMessage: 'ID should be an integer', + }) +}) +``` + +## Example 6: Custom Status Codes +```ts +export default defineEventHandler(async (event) => { + setResponseStatus(event, 202) +}) +``` \ No newline at end of file diff --git a/server/api/v1/health.ts b/server/api/v1/health.ts new file mode 100644 index 0000000..ab9acdf --- /dev/null +++ b/server/api/v1/health.ts @@ -0,0 +1,8 @@ +export default defineEventHandler(async () => { + return { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + memoryUsage: process.memoryUsage().heapUsed*1e-6, // in MB + }; +}) \ No newline at end of file diff --git a/server/api/v1/playlist/[uid].get.ts b/server/api/v1/playlist/[uid].get.ts new file mode 100644 index 0000000..ace864b --- /dev/null +++ b/server/api/v1/playlist/[uid].get.ts @@ -0,0 +1,40 @@ +import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server"; + + +/** + * Endpoint to get a playlist by id + * @returns {Object} - Playlist object + * @throws {400} - Invalid playlistId + * @throws {401} - Unauthenticated + * @throws {500} - Internal Server Error + */ +export default defineEventHandler(async (event) => { + const playlistId = getRouterParam(event, 'uid') + + // check regex playlistId + if (!playlistId || !isValidSpotifyID(playlistId!)) { + setResponseStatus(event, 400); + return {error: 'invalid playlistId'}; + } + + // Require user to be authenticated + const user = await serverSupabaseUser(event); + if (!user?.id) { + setResponseStatus(event, 401); + return {error: 'unauthenticated'}; + } + + const client = serverSupabaseServiceRole(event); + const {data, error} = await client.from('playlists').select('*, categories (name)').eq('id', playlistId).single(); + + if (error) { + setResponseStatus(event, 500); + return {error: error.message}; + } else { + const transformedData = { + ...data, + categories: data.categories.map(category => category.name) + }; + return transformedData; + } +}) \ No newline at end of file diff --git a/server/api/v1/playlist/index.get.ts b/server/api/v1/playlist/index.get.ts new file mode 100644 index 0000000..c1e3d9b --- /dev/null +++ b/server/api/v1/playlist/index.get.ts @@ -0,0 +1,35 @@ +import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server"; + +/** + * Endpoint to get all playlists + * @throws {401} - Unauthenticated + * @throws {500} - Internal Server Error + * @returns {Array} - Array of playlists + */ +export default defineEventHandler(async (event) => { + + // Require user to be authenticated + const user = await serverSupabaseUser(event); + if (!user?.id) { + setResponseStatus(event, 401); + return {error: 'unauthenticated'}; + } + + const client = serverSupabaseServiceRole(event); + const {data, error} = await client.from('playlists') + .select(` + *, + categories (name) + `); + + if (error) { + setResponseStatus(event, 500); + return {error: error.message}; + } else { + const transformedData = data.map(playlist => ({ + ...playlist, + categories: playlist.categories.map(category => category.name), + })); + return transformedData; + } +}) \ No newline at end of file diff --git a/server/api/v1/playlist/index.post.ts b/server/api/v1/playlist/index.post.ts new file mode 100644 index 0000000..7131877 --- /dev/null +++ b/server/api/v1/playlist/index.post.ts @@ -0,0 +1,70 @@ +import {z} from 'zod' +import {spotifyIDRegex} from "~/server/utils/data-validation"; +import {serverSupabaseServiceRole} from "#supabase/server"; +import {UNIQUE_VIOLATION} from "~/server/utils/postgres-errors"; +import {getPlaylistCover, getSpotifyToken} from "~/server/utils/spotify"; + +const schema = z.object({ + id: z.string().regex(spotifyIDRegex), + name: z.string(), + spotifyId: z.string().regex(spotifyIDRegex), + categories: z.array(z.string()), + enabled: z.boolean().optional().default(true) +}) + + +/** + * Unauthenticated endpoint to create a playlist - management only + * @throws {400} - Invalid body + * @throws {400} - Playlist with this ID already exists + * @throws {500} - Internal Server Error + * @returns {Object} - Created playlist + */ +export default defineEventHandler(async (event) => { + const result = await readValidatedBody(event, body => schema.safeParse(body)) + + if (!result.success) { + setResponseStatus(event, 400); + return {error: result.error.issues}; + } + + const token = await getSpotifyToken(); + const coverUrl = await getPlaylistCover(token, result.data.spotifyId); + + const playlistInsert = { + id: result.data.id, + name: result.data.name, + spotifyId: result.data.spotifyId, + cover: coverUrl + } + + const categoriesInsert = result.data.categories.map((category) => ({ + playlistId: result.data.id, + name: category, + })); + + const client = serverSupabaseServiceRole(event); + const {data: playlistData, error: playlistError} = await client.from('playlists').insert(playlistInsert as never).select().single(); //todo: fix type error! + + + if (playlistError) { + setResponseStatus(event, 400); + if (playlistError.code === UNIQUE_VIOLATION) + return {error: 'Playlist with this ID already exists'}; + setResponseStatus(event, 500); + return {error: playlistError.message}; + } + + const { data: categoriesData, error: categoriesError } = await client + .from('categories') + .insert(categoriesInsert as never); + + if (categoriesError) { + setResponseStatus(event, 500); + return { error: `Error inserting categories: ${categoriesError.message}` }; + } + + + setResponseStatus(event, 201); + return { playlist: playlistData, categories: categoriesData }; +}) \ No newline at end of file diff --git a/server/api/v1/playlist/playlist.http b/server/api/v1/playlist/playlist.http new file mode 100644 index 0000000..629a587 --- /dev/null +++ b/server/api/v1/playlist/playlist.http @@ -0,0 +1,36 @@ +@baseUrl = http://localhost:3000/api/v1 +@authCookie = + +### Get all Playlists +GET {{baseUrl}}/playlist +Cookie: {{authCookie}} + + +### Get specific Playlist +@playlistId = 37i9dQZF1EIYE32WUF6sxN + +GET {{baseUrl}}/playlist/{{playlistId}} +Cookie: {{authCookie}} + + +### Add a Spotify Playlist to our system +POST {{baseUrl}}/playlist + +{ +"id": "37i9dQZF1DX2CtuHQcongT", +"name": "This is SEGA SOUND TEAM", +"spotifyId": "37i9dQZF1DX2CtuHQcongT", +"categories": ["sega"] +} + +### Add a Disabled Spotify Playlist to our system +POST {{baseUrl}}/playlist + +{ +"id": "37i9dQZF1DX2CtuHQcongT", +"name": "This is SEGA SOUND TEAM", +"spotifyId": "37i9dQZF1DX2CtuHQcongT", +"categories": ["sega"] +"enabled": false +} + diff --git a/server/api/v1/user/[uid].get.ts b/server/api/v1/user/[uid].get.ts new file mode 100644 index 0000000..f42c743 --- /dev/null +++ b/server/api/v1/user/[uid].get.ts @@ -0,0 +1,5 @@ +export default defineEventHandler((event) => { + const userId = getRouterParam(event, 'uid') + + return; +}) \ No newline at end of file diff --git a/server/utils/data-validation.ts b/server/utils/data-validation.ts new file mode 100644 index 0000000..d8d2876 --- /dev/null +++ b/server/utils/data-validation.ts @@ -0,0 +1,13 @@ +/** + * Spotify ID regex + */ +export const spotifyIDRegex = /^[a-zA-Z0-9]+$/; // base62 + +/** + * Check if a string is a valid Spotify ID + * @param spotifyID - The string to check + * @returns Whether the string is a valid Spotify ID + */ +export function isValidSpotifyID(spotifyID: string): boolean { + return spotifyID.match(spotifyIDRegex) !== null; // base62 +} \ No newline at end of file diff --git a/server/utils/postgres-errors.ts b/server/utils/postgres-errors.ts new file mode 100644 index 0000000..bbc1982 --- /dev/null +++ b/server/utils/postgres-errors.ts @@ -0,0 +1,4 @@ +export const UNIQUE_VIOLATION = '23505'; +export const FOREIGN_KEY_VIOLATION = '23503'; +export const NOT_NULL_VIOLATION = '23502'; + diff --git a/server/utils/spotify.ts b/server/utils/spotify.ts new file mode 100644 index 0000000..1f07a71 --- /dev/null +++ b/server/utils/spotify.ts @@ -0,0 +1,47 @@ +let spotifyToken: string | null = null +let tokenExpiry: number = 0 + +export async function getSpotifyToken() { + // Return cached token if valid + if (spotifyToken && tokenExpiry > Date.now()) { + return spotifyToken + } + + const auth = Buffer + .from(`${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`) + .toString('base64') + + try { + const res = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'grant_type=client_credentials' + }) + + const data = await res.json() + spotifyToken = data.access_token + tokenExpiry = Date.now() + (data.expires_in - 60) * 1000 + + return spotifyToken + } catch (error) { + console.error('Spotify token error:', error) + throw createError({ + statusCode: 500, + message: 'Failed to get Spotify access token' + }) + } +} + +export async function getPlaylistCover(token: string | null, playlistId: string): Promise { + if (!token) return undefined; + + const res = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/images`, { + headers: {'Authorization': `Bearer ${token}`} + }) + const data = await res.json() + // console.log(data) + return data[0].url; +} \ No newline at end of file diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..df650a3 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,20 @@ +{ + "entryPoints": ["./"], + "entryPointStrategy": "expand", + "exclude": [ + "node_modules/**/*", + "dist/**/*", + ".nuxt/**/*", + ".output/**/*" + ], + "excludePrivate": true, + "excludeProtected": true, + "excludeExternals": true, + "includeVersion": true, + "out": "docs", + "plugin": [], + "readme": "README.md", + "theme": "default", + "tsconfig": "./tsconfig.json", + "hideGenerator": true +} \ No newline at end of file