From da255905da981fba3b1d050d6b180ea2be8e25b2 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Thu, 17 Oct 2024 08:46:53 +0200 Subject: [PATCH 01/33] feat(api): Add sample code for features with high usage likelihood --- server/api/v1/README.md | 80 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 server/api/v1/README.md 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 From 8e5404ca4fce06115d1278d673aff05ac4279eca Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Fri, 18 Oct 2024 10:14:16 +0200 Subject: [PATCH 02/33] feat(api): Add Playlist & User Endpoint --- nuxt.config.ts | 3 ++- server/api/v1/health.ts | 8 ++++++++ server/api/v1/playlist/[uid].get.ts | 21 +++++++++++++++++++++ server/api/v1/user/[uid].get.ts | 5 +++++ server/utils/data-validation.ts | 3 +++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 server/api/v1/health.ts create mode 100644 server/api/v1/playlist/[uid].get.ts create mode 100644 server/api/v1/user/[uid].get.ts create mode 100644 server/utils/data-validation.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index 7ff5887..e3d0745 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -17,5 +17,6 @@ export default defineNuxtConfig({ }, devServer: { host: '0.0.0.0' - } + }, + ssr: false }) \ 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..3a13d4c --- /dev/null +++ b/server/api/v1/playlist/[uid].get.ts @@ -0,0 +1,21 @@ +import {serverSupabaseUser} from "#supabase/server"; + +export default defineEventHandler(async (event) => { + const playlistId = getRouterParam(event, 'uid') + + // check regex playlistId + if (!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'}; + } + + + return user; +}) \ No newline at end of file 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..d4c5cda --- /dev/null +++ b/server/utils/data-validation.ts @@ -0,0 +1,3 @@ +export function isValidSpotifyID(spotifyID: string): boolean { + return spotifyID.match(/^[a-zA-Z0-9]+$/) !== null; // base62 +} \ No newline at end of file From 9396b44fd111e2e3cf3c6d014116ef6bf086a089 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Thu, 31 Oct 2024 10:20:33 +0100 Subject: [PATCH 03/33] chore(docs): add typedoc --- package-lock.json | 642 +++++++++++++++++++++++++++++++- package.json | 11 +- server/utils/data-validation.ts | 12 +- typedoc.json | 20 + 4 files changed, 675 insertions(+), 10 deletions(-) create mode 100644 typedoc.json 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/utils/data-validation.ts b/server/utils/data-validation.ts index d4c5cda..d8d2876 100644 --- a/server/utils/data-validation.ts +++ b/server/utils/data-validation.ts @@ -1,3 +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(/^[a-zA-Z0-9]+$/) !== null; // base62 + return spotifyID.match(spotifyIDRegex) !== null; // base62 } \ 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 From 4f14f2f71edfd761d14093b51110e0515f126b51 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Thu, 31 Oct 2024 11:46:08 +0100 Subject: [PATCH 04/33] feat(api): Implement playlist endpoint --- .env.example | 3 ++ server/api/v1/playlist/[uid].get.ts | 21 +++++++++-- server/api/v1/playlist/index.get.ts | 27 ++++++++++++++ server/api/v1/playlist/index.post.ts | 55 ++++++++++++++++++++++++++++ server/api/v1/playlist/playlist.http | 36 ++++++++++++++++++ server/utils/postgres-errors.ts | 4 ++ server/utils/spotify.ts | 46 +++++++++++++++++++++++ 7 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 server/api/v1/playlist/index.get.ts create mode 100644 server/api/v1/playlist/index.post.ts create mode 100644 server/api/v1/playlist/playlist.http create mode 100644 server/utils/postgres-errors.ts create mode 100644 server/utils/spotify.ts diff --git a/.env.example b/.env.example index a303c20..943a364 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" +NUXT_SPOTIFY_CLIENT_ID="your-client-id" +NUXT_SPOTIFY_CLIENT_SECRET="your-client-secret" \ No newline at end of file diff --git a/server/api/v1/playlist/[uid].get.ts b/server/api/v1/playlist/[uid].get.ts index 3a13d4c..b9be938 100644 --- a/server/api/v1/playlist/[uid].get.ts +++ b/server/api/v1/playlist/[uid].get.ts @@ -1,10 +1,18 @@ -import {serverSupabaseUser} from "#supabase/server"; +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 (!isValidSpotifyID(playlistId)) { + if (!playlistId || !isValidSpotifyID(playlistId!)) { setResponseStatus(event, 400); return {error: 'invalid playlistId'}; } @@ -16,6 +24,13 @@ export default defineEventHandler(async (event) => { return {error: 'unauthenticated'}; } + const client = serverSupabaseServiceRole(event); + const {data, error} = await client.from('playlists').select('*').eq('id', playlistId).single(); + + if (error) { + setResponseStatus(event, 500); + return {error: error.message}; + } - return user; + return data; }) \ 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..1a23cb1 --- /dev/null +++ b/server/api/v1/playlist/index.get.ts @@ -0,0 +1,27 @@ +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(); + + if (error) { + setResponseStatus(event, 500); + return {error: error.message}; + } + + return data; +}) \ 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..8a756a2 --- /dev/null +++ b/server/api/v1/playlist/index.post.ts @@ -0,0 +1,55 @@ +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() +}) + + +/** + * 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 client = serverSupabaseServiceRole(event) + const {data, error} = await client.from('playlists').insert(playlistInsert as never).select().single(); //todo: fix type error! + + if (error) { + setResponseStatus(event, 400); + if (error.code === UNIQUE_VIOLATION) + return {error: 'Playlist with this ID already exists'}; + setResponseStatus(event, 500); + return {error: error.message}; + } + + + setResponseStatus(event, 201); + return data; +}) \ 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..e958f8c --- /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": "37i9dQZF1EIYE32WUF6sxN", +"name": "Hardstyle Popular", +"spotifyId": "37i9dQZF1EIYE32WUF6sxN", +"categories": ["hardstyle", "remix"] +} + +### Add a Disabled Spotify Playlist to our system +POST {{baseUrl}}/playlist + +{ +"id": "4DC0uhUb7itYVuVVF3goSN", +"name": "Memes", +"spotifyId": "4DC0uhUb7itYVuVVF3goSN", +"categories": ["meme"], +"enabled": false +} + 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..3639c9f --- /dev/null +++ b/server/utils/spotify.ts @@ -0,0 +1,46 @@ +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}?fields=images`, { + headers: {'Authorization': `Bearer ${token}`} + }) + const data = await res.json() + return data.images[0].url; +} \ No newline at end of file From d8dd8454d037d1cec93e9e126a0e7ddcbd2051bc Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Thu, 7 Nov 2024 13:15:38 +0100 Subject: [PATCH 05/33] chore(readme): Update Readme and .env example to match current state of app --- .env.example | 4 ++-- README.md | 37 +++---------------------------------- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index 943a364..f50895e 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ SUPABASE_URL="https://yourhost.supabase.co" SUPABASE_KEY="your-secret-key" SUPABASE_SERVICE_KEY="your-service-key" -NUXT_SPOTIFY_CLIENT_ID="your-client-id" -NUXT_SPOTIFY_CLIENT_SECRET="your-client-secret" \ No newline at end of file +SPOTIFY_CLIENT_ID="your-client-id" +SPOTIFY_CLIENT_SECRET="your-client-secret" \ No newline at end of file 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 From ff4e5c8576d48cc54ed65c4a394e710549a64e29 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Fri, 15 Nov 2024 11:41:09 +0100 Subject: [PATCH 06/33] fix(api): Playlists were disabled on creation --- nuxt.config.ts | 5 +++++ server/api/v1/playlist/index.post.ts | 2 +- server/api/v1/playlist/playlist.http | 16 ++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index e3d0745..debde16 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -13,6 +13,11 @@ export default defineNuxtConfig({ }, cookieOptions: { secure: false, //TODO: set to true when deploying + }, + clientOptions: { + db: { + schema: 'beatbuzzer', + } } }, devServer: { diff --git a/server/api/v1/playlist/index.post.ts b/server/api/v1/playlist/index.post.ts index 8a756a2..7fa2412 100644 --- a/server/api/v1/playlist/index.post.ts +++ b/server/api/v1/playlist/index.post.ts @@ -9,7 +9,7 @@ const schema = z.object({ name: z.string(), spotifyId: z.string().regex(spotifyIDRegex), categories: z.array(z.string()), - enabled: z.boolean().optional() + enabled: z.boolean().optional().default(true) }) diff --git a/server/api/v1/playlist/playlist.http b/server/api/v1/playlist/playlist.http index e958f8c..629a587 100644 --- a/server/api/v1/playlist/playlist.http +++ b/server/api/v1/playlist/playlist.http @@ -17,20 +17,20 @@ Cookie: {{authCookie}} POST {{baseUrl}}/playlist { -"id": "37i9dQZF1EIYE32WUF6sxN", -"name": "Hardstyle Popular", -"spotifyId": "37i9dQZF1EIYE32WUF6sxN", -"categories": ["hardstyle", "remix"] +"id": "37i9dQZF1DX2CtuHQcongT", +"name": "This is SEGA SOUND TEAM", +"spotifyId": "37i9dQZF1DX2CtuHQcongT", +"categories": ["sega"] } ### Add a Disabled Spotify Playlist to our system POST {{baseUrl}}/playlist { -"id": "4DC0uhUb7itYVuVVF3goSN", -"name": "Memes", -"spotifyId": "4DC0uhUb7itYVuVVF3goSN", -"categories": ["meme"], +"id": "37i9dQZF1DX2CtuHQcongT", +"name": "This is SEGA SOUND TEAM", +"spotifyId": "37i9dQZF1DX2CtuHQcongT", +"categories": ["sega"] "enabled": false } From 7c5d31086276ea416928e1753158d13900038bf4 Mon Sep 17 00:00:00 2001 From: synan798 Date: Sat, 30 Nov 2024 00:23:50 +0100 Subject: [PATCH 07/33] added categories to get requests --- server/api/v1/playlist/[uid].get.ts | 10 +++++++--- server/api/v1/playlist/index.get.ts | 14 +++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/server/api/v1/playlist/[uid].get.ts b/server/api/v1/playlist/[uid].get.ts index b9be938..ace864b 100644 --- a/server/api/v1/playlist/[uid].get.ts +++ b/server/api/v1/playlist/[uid].get.ts @@ -25,12 +25,16 @@ export default defineEventHandler(async (event) => { } const client = serverSupabaseServiceRole(event); - const {data, error} = await client.from('playlists').select('*').eq('id', playlistId).single(); + 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; } - - return data; }) \ No newline at end of file diff --git a/server/api/v1/playlist/index.get.ts b/server/api/v1/playlist/index.get.ts index 1a23cb1..c1e3d9b 100644 --- a/server/api/v1/playlist/index.get.ts +++ b/server/api/v1/playlist/index.get.ts @@ -16,12 +16,20 @@ export default defineEventHandler(async (event) => { } const client = serverSupabaseServiceRole(event); - const {data, error} = await client.from('playlists').select(); + 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; } - - return data; }) \ No newline at end of file From 3d511f30c5e151d889922d6302b107f8320ba8f5 Mon Sep 17 00:00:00 2001 From: synan798 Date: Sat, 30 Nov 2024 12:37:31 +0100 Subject: [PATCH 08/33] added categories to post request (getPlaylistCover doesn't work) --- server/api/v1/playlist/index.post.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/server/api/v1/playlist/index.post.ts b/server/api/v1/playlist/index.post.ts index 7fa2412..d53e039 100644 --- a/server/api/v1/playlist/index.post.ts +++ b/server/api/v1/playlist/index.post.ts @@ -21,6 +21,7 @@ const schema = z.object({ * @returns {Object} - Created playlist */ export default defineEventHandler(async (event) => { + // fix coverUrl and delete test.vue const result = await readValidatedBody(event, body => schema.safeParse(body)) if (!result.success) { @@ -36,11 +37,18 @@ export default defineEventHandler(async (event) => { name: result.data.name, spotifyId: result.data.spotifyId, cover: coverUrl + //cover: "https://i.scdn.co/image/ab67706f000000024d183558628c25f8cb314eea" } - const client = serverSupabaseServiceRole(event) + const categoriesInsert = result.data.categories.map((category) => ({ + playlistId: result.data.id, + name: category, + })); + + const client = serverSupabaseServiceRole(event); const {data, error} = await client.from('playlists').insert(playlistInsert as never).select().single(); //todo: fix type error! + if (error) { setResponseStatus(event, 400); if (error.code === UNIQUE_VIOLATION) @@ -49,6 +57,15 @@ export default defineEventHandler(async (event) => { return {error: error.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 data; From 05ef067c168c30127503078ed59dd9a5b85f2668 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Sat, 30 Nov 2024 12:42:18 +0100 Subject: [PATCH 09/33] feat(friends): Start implementing database layer --- DB/friendships.sql | 122 +++++++++++++++++++++ DB/users.sql | 15 +++ server/api/v1/user/friends/index.delete.ts | 1 + server/api/v1/user/friends/index.get.ts | 1 + server/api/v1/user/friends/index.post.ts | 1 + server/api/v1/user/friends/index.put.ts | 1 + 6 files changed, 141 insertions(+) create mode 100644 DB/friendships.sql create mode 100644 DB/users.sql create mode 100644 server/api/v1/user/friends/index.delete.ts create mode 100644 server/api/v1/user/friends/index.get.ts create mode 100644 server/api/v1/user/friends/index.post.ts create mode 100644 server/api/v1/user/friends/index.put.ts diff --git a/DB/friendships.sql b/DB/friendships.sql new file mode 100644 index 0000000..711151d --- /dev/null +++ b/DB/friendships.sql @@ -0,0 +1,122 @@ +CREATE TYPE friendship_status AS ENUM ('pending', 'accepted', 'declined', 'blocked'); + +CREATE TABLE friendships +( + friendship_id BIGSERIAL PRIMARY KEY, + user1_id UUID NOT NULL, + user2_id UUID NOT NULL, + status friendship_status NOT NULL, + action_user_id UUID NOT NULL, -- The user who performed the last action + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + + -- Ensure user1_id is always less than user2_id to prevent duplicate friendships + CONSTRAINT ensure_user_order CHECK (user1_id < user2_id), + CONSTRAINT unique_friendship UNIQUE (user1_id, user2_id), + + -- Foreign keys + CONSTRAINT fk_user1 FOREIGN KEY (user1_id) REFERENCES users (id), + CONSTRAINT fk_user2 FOREIGN KEY (user2_id) REFERENCES users (id), + CONSTRAINT fk_action_user FOREIGN KEY (action_user_id) REFERENCES users (id) +); + +CREATE INDEX idx_friendship_user1 ON friendships (user1_id, status); +CREATE INDEX idx_friendship_user2 ON friendships (user2_id, status); + +-- Functions + +-- automatically update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() + RETURNS TRIGGER AS +$$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$; + +CREATE TRIGGER update_friendships_timestamp + BEFORE UPDATE + ON friendships + FOR EACH ROW +EXECUTE FUNCTION update_updated_at_column(); + +-- Send a friend request +CREATE OR REPLACE FUNCTION send_friend_request(sender_id UUID, receiver_id UUID) RETURNS void AS +$$ +DECLARE + smaller_id UUID; + larger_id UUID; +BEGIN + -- Determine order of IDs using UUID comparison + IF sender_id < receiver_id THEN + smaller_id := sender_id; + larger_id := receiver_id; + ELSE + smaller_id := receiver_id; + larger_id := sender_id; + END IF; + + -- Insert friendship record + INSERT INTO friendships (user1_id, user2_id, status, action_user_id) + VALUES (smaller_id, larger_id, 'pending', sender_id) + ON CONFLICT (user1_id, user2_id) DO UPDATE + SET status = CASE + WHEN friendships.status = 'declined' THEN 'pending'::friendship_status + ELSE friendships.status + END, + action_user_id = sender_id; +END; +$$ LANGUAGE plpgsql; + +-- retrieve incoming friend requests +CREATE OR REPLACE FUNCTION get_incoming_friend_requests(user_id UUID) + RETURNS TABLE + ( + friendship_id BIGINT, + sender_id UUID, + receiver_id UUID, + status friendship_status, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE + ) +AS +$$ +BEGIN + RETURN QUERY + SELECT f.friendship_id, + CASE + WHEN f.user1_id = user_id THEN f.user2_id + ELSE f.user1_id + END AS sender_id, + user_id AS receiver_id, + f.status, + f.created_at, + f.updated_at + FROM friendships f + WHERE (f.user1_id = user_id OR f.user2_id = user_id) + AND f.status = 'pending' + AND f.action_user_id != user_id; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION accept_friend_request_by_id(friendship_id_param BIGINT) + RETURNS void AS +$$ +BEGIN + UPDATE friendships + SET status = 'accepted' + WHERE friendship_id = friendship_id_param + AND status = 'pending'; + + IF NOT FOUND THEN + RAISE EXCEPTION 'No pending friend request found with this ID'; + END IF; +END; +$$ LANGUAGE plpgsql; + + +-- examples +SELECT * +FROM get_incoming_friend_requests('d2e6a9a3-a0be-45ce-ae39-676c6a88c53a'); +SELECT accept_friend_request_by_id(4::bigint); \ No newline at end of file diff --git a/DB/users.sql b/DB/users.sql new file mode 100644 index 0000000..ef78b16 --- /dev/null +++ b/DB/users.sql @@ -0,0 +1,15 @@ +CREATE TABLE users +( + id uuid not null references auth.users on delete cascade, + avatar_url text, + username text not null, + spotify_id text not null, + spotify_visibility boolean not null default false, + primary key (id) +); + +ALTER TABLE users + ADD CONSTRAINT unique_username UNIQUE (username), + ADD CONSTRAINT valid_username check (username <> '' AND length(trim(username)) >= 4 AND username ~ '^[a-zA-Z0-9_]+$'); + +CREATE INDEX idx_username ON users(username); \ No newline at end of file diff --git a/server/api/v1/user/friends/index.delete.ts b/server/api/v1/user/friends/index.delete.ts new file mode 100644 index 0000000..950c339 --- /dev/null +++ b/server/api/v1/user/friends/index.delete.ts @@ -0,0 +1 @@ +// remove friend \ No newline at end of file diff --git a/server/api/v1/user/friends/index.get.ts b/server/api/v1/user/friends/index.get.ts new file mode 100644 index 0000000..2efc70f --- /dev/null +++ b/server/api/v1/user/friends/index.get.ts @@ -0,0 +1 @@ +// Not relative to userid because you should always only be able to see your own friends \ No newline at end of file diff --git a/server/api/v1/user/friends/index.post.ts b/server/api/v1/user/friends/index.post.ts new file mode 100644 index 0000000..2ab0ba0 --- /dev/null +++ b/server/api/v1/user/friends/index.post.ts @@ -0,0 +1 @@ +// send friend invite \ No newline at end of file diff --git a/server/api/v1/user/friends/index.put.ts b/server/api/v1/user/friends/index.put.ts new file mode 100644 index 0000000..d5fe53c --- /dev/null +++ b/server/api/v1/user/friends/index.put.ts @@ -0,0 +1 @@ +// accept or decline friend request \ No newline at end of file From a8a731d0daae502c127af6a2e0b753b3377d1be1 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Sat, 30 Nov 2024 12:53:38 +0100 Subject: [PATCH 10/33] fix(playlist): Fix playlist cover not loading --- server/api/v1/playlist/index.post.ts | 2 -- server/utils/spotify.ts | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server/api/v1/playlist/index.post.ts b/server/api/v1/playlist/index.post.ts index d53e039..d22a410 100644 --- a/server/api/v1/playlist/index.post.ts +++ b/server/api/v1/playlist/index.post.ts @@ -21,7 +21,6 @@ const schema = z.object({ * @returns {Object} - Created playlist */ export default defineEventHandler(async (event) => { - // fix coverUrl and delete test.vue const result = await readValidatedBody(event, body => schema.safeParse(body)) if (!result.success) { @@ -37,7 +36,6 @@ export default defineEventHandler(async (event) => { name: result.data.name, spotifyId: result.data.spotifyId, cover: coverUrl - //cover: "https://i.scdn.co/image/ab67706f000000024d183558628c25f8cb314eea" } const categoriesInsert = result.data.categories.map((category) => ({ diff --git a/server/utils/spotify.ts b/server/utils/spotify.ts index 3639c9f..1f07a71 100644 --- a/server/utils/spotify.ts +++ b/server/utils/spotify.ts @@ -38,9 +38,10 @@ export async function getSpotifyToken() { 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}?fields=images`, { + const res = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/images`, { headers: {'Authorization': `Bearer ${token}`} }) const data = await res.json() - return data.images[0].url; + // console.log(data) + return data[0].url; } \ No newline at end of file From d6f80fa424b910881bed29840409f32283f1da4d Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Sat, 30 Nov 2024 13:05:24 +0100 Subject: [PATCH 11/33] feat(playlist): Add DDL for playlist database schema --- DB/playlists.sql | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 DB/playlists.sql 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") +); From c6a32f28d17c061d4b623d8e4a5f48b261cc9c31 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Sat, 30 Nov 2024 13:28:22 +0100 Subject: [PATCH 12/33] feat(friends): Add decline db function --- DB/friendships.sql | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/DB/friendships.sql b/DB/friendships.sql index 711151d..7e52344 100644 --- a/DB/friendships.sql +++ b/DB/friendships.sql @@ -1,4 +1,4 @@ -CREATE TYPE friendship_status AS ENUM ('pending', 'accepted', 'declined', 'blocked'); +CREATE TYPE friendship_status AS ENUM ('pending', 'accepted', 'declined'); CREATE TABLE friendships ( @@ -33,7 +33,7 @@ BEGIN NEW.updated_at = now(); RETURN NEW; END; -$$; +$$ LANGUAGE plpgsql; CREATE TRIGGER update_friendships_timestamp BEFORE UPDATE @@ -116,7 +116,24 @@ END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION decline_friend_request_by_id(friendship_id_param BIGINT) + RETURNS void AS +$$ +BEGIN + UPDATE friendships + SET status = 'declined' + WHERE friendship_id = friendship_id_param + AND status = 'pending'; + + IF NOT FOUND THEN + RAISE EXCEPTION 'No pending friend request found with this ID'; + END IF; +END; +$$ LANGUAGE plpgsql; + + -- examples -SELECT * -FROM get_incoming_friend_requests('d2e6a9a3-a0be-45ce-ae39-676c6a88c53a'); -SELECT accept_friend_request_by_id(4::bigint); \ No newline at end of file +SELECT send_friend_request('sender', 'receiver'); +SELECT * FROM get_incoming_friend_requests('d2e6a9a3-a0be-45ce-ae39-676c6a88c53a'); +SELECT accept_friend_request_by_id(4); +SELECT decline_friend_request_by_id(4); From 90aaf2d3f9f65fa94447e516c1141974ac5898b2 Mon Sep 17 00:00:00 2001 From: synan798 Date: Sat, 30 Nov 2024 16:15:27 +0100 Subject: [PATCH 13/33] added correct return statement --- server/api/v1/playlist/index.post.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/api/v1/playlist/index.post.ts b/server/api/v1/playlist/index.post.ts index d22a410..7131877 100644 --- a/server/api/v1/playlist/index.post.ts +++ b/server/api/v1/playlist/index.post.ts @@ -44,15 +44,15 @@ export default defineEventHandler(async (event) => { })); const client = serverSupabaseServiceRole(event); - const {data, error} = await client.from('playlists').insert(playlistInsert as never).select().single(); //todo: fix type error! + const {data: playlistData, error: playlistError} = await client.from('playlists').insert(playlistInsert as never).select().single(); //todo: fix type error! - if (error) { + if (playlistError) { setResponseStatus(event, 400); - if (error.code === UNIQUE_VIOLATION) + if (playlistError.code === UNIQUE_VIOLATION) return {error: 'Playlist with this ID already exists'}; setResponseStatus(event, 500); - return {error: error.message}; + return {error: playlistError.message}; } const { data: categoriesData, error: categoriesError } = await client @@ -66,5 +66,5 @@ export default defineEventHandler(async (event) => { setResponseStatus(event, 201); - return data; + return { playlist: playlistData, categories: categoriesData }; }) \ No newline at end of file From 240f4f63aca80108af2f94754f83857097f88e36 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Mon, 2 Dec 2024 20:18:33 +0100 Subject: [PATCH 14/33] feat(friends): add on delete cascade for user deletion and finished sql friendship functions --- DB/friendships.sql | 72 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/DB/friendships.sql b/DB/friendships.sql index 7e52344..03eabcf 100644 --- a/DB/friendships.sql +++ b/DB/friendships.sql @@ -15,8 +15,8 @@ CREATE TABLE friendships CONSTRAINT unique_friendship UNIQUE (user1_id, user2_id), -- Foreign keys - CONSTRAINT fk_user1 FOREIGN KEY (user1_id) REFERENCES users (id), - CONSTRAINT fk_user2 FOREIGN KEY (user2_id) REFERENCES users (id), + CONSTRAINT fk_user1 FOREIGN KEY (user1_id) REFERENCES users (id) ON DELETE CASCADE, + CONSTRAINT fk_user2 FOREIGN KEY (user2_id) REFERENCES users (id) ON DELETE CASCADE, CONSTRAINT fk_action_user FOREIGN KEY (action_user_id) REFERENCES users (id) ); @@ -48,7 +48,7 @@ DECLARE smaller_id UUID; larger_id UUID; BEGIN - -- Determine order of IDs using UUID comparison + -- Determine order of IDs IF sender_id < receiver_id THEN smaller_id := sender_id; larger_id := receiver_id; @@ -100,6 +100,7 @@ BEGIN END; $$ LANGUAGE plpgsql; +-- accept friendship request CREATE OR REPLACE FUNCTION accept_friend_request_by_id(friendship_id_param BIGINT) RETURNS void AS $$ @@ -116,6 +117,7 @@ END; $$ LANGUAGE plpgsql; +-- decline friendship request CREATE OR REPLACE FUNCTION decline_friend_request_by_id(friendship_id_param BIGINT) RETURNS void AS $$ @@ -132,8 +134,64 @@ END; $$ LANGUAGE plpgsql; +-- retrieve accepted friendships +CREATE OR REPLACE FUNCTION get_friends(user_id UUID) + RETURNS TABLE + ( + friendship_id BIGINT, + friend_id UUID, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE + ) +AS +$$ +BEGIN + RETURN QUERY + SELECT f.friendship_id, + CASE + WHEN f.user1_id = user_id THEN f.user2_id + ELSE f.user1_id + END AS friend_id, + f.created_at, + f.updated_at + FROM friendships f + WHERE (f.user1_id = user_id OR f.user2_id = user_id) + AND f.status = 'accepted'; +END; +$$ LANGUAGE plpgsql; + +-- delete friendship +CREATE OR REPLACE FUNCTION remove_friend(user_id UUID, friend_id UUID) RETURNS void AS +$$ +DECLARE + smaller_id UUID; + larger_id UUID; +BEGIN + -- Determine order of IDs + IF friend_id < user_id THEN + smaller_id := friend_id; + larger_id := user_id; + ELSE + smaller_id := user_id; + larger_id := friend_id; + END IF; + + DELETE + FROM friendships + WHERE user1_id = smaller_id + AND user2_id = larger_id + AND status = 'accepted'; + + IF NOT FOUND THEN + RAISE EXCEPTION 'No active friendship found between these users'; + END IF; +END; +$$ LANGUAGE plpgsql; + -- examples -SELECT send_friend_request('sender', 'receiver'); -SELECT * FROM get_incoming_friend_requests('d2e6a9a3-a0be-45ce-ae39-676c6a88c53a'); -SELECT accept_friend_request_by_id(4); -SELECT decline_friend_request_by_id(4); +-- SELECT send_friend_request('sender', 'receiver'); +-- SELECT * FROM get_incoming_friend_requests('user_id'); +-- SELECT accept_friend_request_by_id(4); +-- SELECT decline_friend_request_by_id(4); +-- SELECT * FROM get_friends('user_id'); +-- SELECT remove_friend('friend_user'); From 06ef470dc7f54d5b2f87b911353966679646edf1 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Mon, 2 Dec 2024 20:21:48 +0100 Subject: [PATCH 15/33] feat(friends): implement GET `/user/friends` endpoint --- server/api/v1/user/friends/index.get.ts | 32 +++++++++++++++++++- tsconfig.json | 7 ++++- types/api/responses/user.friends.response.ts | 14 +++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 types/api/responses/user.friends.response.ts diff --git a/server/api/v1/user/friends/index.get.ts b/server/api/v1/user/friends/index.get.ts index 2efc70f..69e8b52 100644 --- a/server/api/v1/user/friends/index.get.ts +++ b/server/api/v1/user/friends/index.get.ts @@ -1 +1,31 @@ -// Not relative to userid because you should always only be able to see your own friends \ No newline at end of file +import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server"; +import type {FriendError, GetFriendParam, GetFriendsResponse} from "~/types/api/responses/user.friends.response"; + +// Not relative to userid because you should always only be able to see your own friends +// User id can be grabbed from the access token + +export default defineEventHandler(async (event) => { + // Require user to be authenticated + const user = await serverSupabaseUser(event); + if (!user?.id) { + setResponseStatus(event, 401); + return {error: 'unauthenticated'}; + } + + // Get friends + const client = serverSupabaseServiceRole(event); + const param: GetFriendParam = {user_id: user.id}; + + const {data, error}: { + data: GetFriendsResponse | null, + error: FriendError | null + } = await client.rpc('get_friends', param as never); + + // Handle errors + if (error) { + setResponseStatus(event, 500); + return {error: error.message}; + } + + return data; +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a746f2a..f72ecb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,9 @@ { // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json" + "extends": "./.nuxt/tsconfig.json", + "compilerOptions": { + "paths": { + "@/*": ["./*"] + } + }, } diff --git a/types/api/responses/user.friends.response.ts b/types/api/responses/user.friends.response.ts new file mode 100644 index 0000000..fdb083f --- /dev/null +++ b/types/api/responses/user.friends.response.ts @@ -0,0 +1,14 @@ +export interface GetFriendsResponse { + friendship_id: bigint, + friend_id: string, + created_at: string + updated_at: string +} + +export interface FriendError { + message: string +} + +export interface GetFriendParam { + user_id: string +} \ No newline at end of file From 3a824d23f158dda77c810818c614a73ed9502483 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Mon, 2 Dec 2024 20:43:11 +0100 Subject: [PATCH 16/33] feat(friends): GET `/user/friends` endpoint also shows outgoing friend requests --- DB/friendships.sql | 10 ++++++---- types/api/responses/user.friends.response.ts | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/DB/friendships.sql b/DB/friendships.sql index 03eabcf..9149339 100644 --- a/DB/friendships.sql +++ b/DB/friendships.sql @@ -134,14 +134,15 @@ END; $$ LANGUAGE plpgsql; --- retrieve accepted friendships +-- retrieve accepted friendships aswell as pending friendships where the user is the action_user CREATE OR REPLACE FUNCTION get_friends(user_id UUID) RETURNS TABLE ( friendship_id BIGINT, friend_id UUID, created_at TIMESTAMP WITH TIME ZONE, - updated_at TIMESTAMP WITH TIME ZONE + updated_at TIMESTAMP WITH TIME ZONE, + status friendship_status ) AS $$ @@ -153,10 +154,11 @@ BEGIN ELSE f.user1_id END AS friend_id, f.created_at, - f.updated_at + f.updated_at, + f.status FROM friendships f WHERE (f.user1_id = user_id OR f.user2_id = user_id) - AND f.status = 'accepted'; + AND (f.status = 'accepted' OR (f.status = 'pending' AND f.action_user_id = user_id)); END; $$ LANGUAGE plpgsql; diff --git a/types/api/responses/user.friends.response.ts b/types/api/responses/user.friends.response.ts index fdb083f..d409702 100644 --- a/types/api/responses/user.friends.response.ts +++ b/types/api/responses/user.friends.response.ts @@ -3,6 +3,7 @@ export interface GetFriendsResponse { friend_id: string, created_at: string updated_at: string + status: "pending" | "accepted" } export interface FriendError { From bd81104a65f770df23171f1da4862b48f24bed7c Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Mon, 2 Dec 2024 20:47:00 +0100 Subject: [PATCH 17/33] feat(friends): implement POST `/user/friends` endpoint for sending friend requests, and moved types file --- server/api/v1/user/friends/index.get.ts | 2 +- server/api/v1/user/friends/index.post.ts | 38 ++++++++++++++++++- ...er.friends.response.ts => user.friends.ts} | 5 +++ 3 files changed, 43 insertions(+), 2 deletions(-) rename types/api/{responses/user.friends.response.ts => user.friends.ts} (75%) diff --git a/server/api/v1/user/friends/index.get.ts b/server/api/v1/user/friends/index.get.ts index 69e8b52..9356818 100644 --- a/server/api/v1/user/friends/index.get.ts +++ b/server/api/v1/user/friends/index.get.ts @@ -1,5 +1,5 @@ import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server"; -import type {FriendError, GetFriendParam, GetFriendsResponse} from "~/types/api/responses/user.friends.response"; +import type {FriendError, GetFriendParam, GetFriendsResponse} from "~/types/api/user.friends"; // Not relative to userid because you should always only be able to see your own friends // User id can be grabbed from the access token diff --git a/server/api/v1/user/friends/index.post.ts b/server/api/v1/user/friends/index.post.ts index 2ab0ba0..fe7bed2 100644 --- a/server/api/v1/user/friends/index.post.ts +++ b/server/api/v1/user/friends/index.post.ts @@ -1 +1,37 @@ -// send friend invite \ No newline at end of file +// send friend invite +import {z} from 'zod' +import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server"; +import type {SendFriendRequestParam} from "~/types/api/user.friends"; + +const userSchema = z.object({ + receiver_id: z.string().uuid() +}) + +export default defineEventHandler(async (event) => { + // validate post-request body + const result = await readValidatedBody(event, body => userSchema.safeParse(body)) + if (!result.success) { + setResponseStatus(event, 400); + return {error: result.error.issues}; + } + + // Require user to be authenticated + const user = await serverSupabaseUser(event); + if (!user?.id) { + setResponseStatus(event, 401); + return {error: 'unauthenticated'}; + } + + // Send request + const client = serverSupabaseServiceRole(event); + const param: SendFriendRequestParam = {sender_id: user.id, receiver_id: result.data.receiver_id}; + const {error} = await client.rpc('send_friend_request', param as never); + + // Handle errors + if (error) { + setResponseStatus(event, 500); + return {error: error.message}; + } + + return {}; +}); \ No newline at end of file diff --git a/types/api/responses/user.friends.response.ts b/types/api/user.friends.ts similarity index 75% rename from types/api/responses/user.friends.response.ts rename to types/api/user.friends.ts index d409702..9bfb869 100644 --- a/types/api/responses/user.friends.response.ts +++ b/types/api/user.friends.ts @@ -12,4 +12,9 @@ export interface FriendError { export interface GetFriendParam { user_id: string +} + +export interface SendFriendRequestParam { + sender_id: string, + receiver_id: string } \ No newline at end of file From 882f065dac98a52ea2a9596c93fc80788d2ace59 Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Mon, 2 Dec 2024 21:41:55 +0100 Subject: [PATCH 18/33] feat(friends): remove unnecessary/redundant DB get functions --- DB/friendships.sql | 113 ++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 68 deletions(-) diff --git a/DB/friendships.sql b/DB/friendships.sql index 9149339..87fa8c4 100644 --- a/DB/friendships.sql +++ b/DB/friendships.sql @@ -1,6 +1,13 @@ -CREATE TYPE friendship_status AS ENUM ('pending', 'accepted', 'declined'); - -CREATE TABLE friendships +DO +$$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'friendship_status') THEN + CREATE TYPE friendship_status AS ENUM ('pending', 'accepted', 'declined'); + END IF; + END +$$; + +CREATE TABLE IF NOT EXISTS friendships ( friendship_id BIGSERIAL PRIMARY KEY, user1_id UUID NOT NULL, @@ -20,8 +27,8 @@ CREATE TABLE friendships CONSTRAINT fk_action_user FOREIGN KEY (action_user_id) REFERENCES users (id) ); -CREATE INDEX idx_friendship_user1 ON friendships (user1_id, status); -CREATE INDEX idx_friendship_user2 ON friendships (user2_id, status); +CREATE INDEX IF NOT EXISTS idx_friendship_user1 ON friendships (user1_id, status); +CREATE INDEX IF NOT EXISTS idx_friendship_user2 ON friendships (user2_id, status); -- Functions @@ -35,7 +42,7 @@ BEGIN END; $$ LANGUAGE plpgsql; -CREATE TRIGGER update_friendships_timestamp +CREATE OR REPLACE TRIGGER update_friendships_timestamp BEFORE UPDATE ON friendships FOR EACH ROW @@ -69,37 +76,6 @@ BEGIN END; $$ LANGUAGE plpgsql; --- retrieve incoming friend requests -CREATE OR REPLACE FUNCTION get_incoming_friend_requests(user_id UUID) - RETURNS TABLE - ( - friendship_id BIGINT, - sender_id UUID, - receiver_id UUID, - status friendship_status, - created_at TIMESTAMP WITH TIME ZONE, - updated_at TIMESTAMP WITH TIME ZONE - ) -AS -$$ -BEGIN - RETURN QUERY - SELECT f.friendship_id, - CASE - WHEN f.user1_id = user_id THEN f.user2_id - ELSE f.user1_id - END AS sender_id, - user_id AS receiver_id, - f.status, - f.created_at, - f.updated_at - FROM friendships f - WHERE (f.user1_id = user_id OR f.user2_id = user_id) - AND f.status = 'pending' - AND f.action_user_id != user_id; -END; -$$ LANGUAGE plpgsql; - -- accept friendship request CREATE OR REPLACE FUNCTION accept_friend_request_by_id(friendship_id_param BIGINT) RETURNS void AS @@ -133,35 +109,6 @@ BEGIN END; $$ LANGUAGE plpgsql; - --- retrieve accepted friendships aswell as pending friendships where the user is the action_user -CREATE OR REPLACE FUNCTION get_friends(user_id UUID) - RETURNS TABLE - ( - friendship_id BIGINT, - friend_id UUID, - created_at TIMESTAMP WITH TIME ZONE, - updated_at TIMESTAMP WITH TIME ZONE, - status friendship_status - ) -AS -$$ -BEGIN - RETURN QUERY - SELECT f.friendship_id, - CASE - WHEN f.user1_id = user_id THEN f.user2_id - ELSE f.user1_id - END AS friend_id, - f.created_at, - f.updated_at, - f.status - FROM friendships f - WHERE (f.user1_id = user_id OR f.user2_id = user_id) - AND (f.status = 'accepted' OR (f.status = 'pending' AND f.action_user_id = user_id)); -END; -$$ LANGUAGE plpgsql; - -- delete friendship CREATE OR REPLACE FUNCTION remove_friend(user_id UUID, friend_id UUID) RETURNS void AS $$ @@ -190,10 +137,40 @@ BEGIN END; $$ LANGUAGE plpgsql; + +-- retrieve all friends, incoming and outgoing friend requests +CREATE OR REPLACE FUNCTION get_friends(user_id UUID) + RETURNS TABLE + ( + friendship_id BIGINT, + friend_id UUID, + status friendship_status, + action_user_id UUID, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE + ) +AS +$$ +BEGIN + RETURN QUERY + SELECT f.friendship_id, + CASE + WHEN f.user1_id = user_id THEN f.user2_id + ELSE f.user1_id + END AS friend_id, + f.status, + f.action_user_id, + f.created_at, + f.updated_at + FROM friendships f + WHERE (f.user1_id = user_id OR f.user2_id = user_id) + AND (f.status != 'declined'); +END; +$$ LANGUAGE plpgsql; + -- examples -- SELECT send_friend_request('sender', 'receiver'); --- SELECT * FROM get_incoming_friend_requests('user_id'); -- SELECT accept_friend_request_by_id(4); -- SELECT decline_friend_request_by_id(4); --- SELECT * FROM get_friends('user_id'); -- SELECT remove_friend('friend_user'); +-- SELECT * FROM get_friends('user_id'); From c2ebc0ccfc2dcacf4e49aadb0b54eddc4cc1e4ab Mon Sep 17 00:00:00 2001 From: synan798 Date: Wed, 4 Dec 2024 09:46:24 +0100 Subject: [PATCH 19/33] added Start Buttons to home --- layouts/HeaderFooterView.vue | 4 ++-- pages/home.vue | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 pages/home.vue diff --git a/layouts/HeaderFooterView.vue b/layouts/HeaderFooterView.vue index 0cecbb1..11cadc7 100644 --- a/layouts/HeaderFooterView.vue +++ b/layouts/HeaderFooterView.vue @@ -15,8 +15,8 @@ -
- +
+
diff --git a/pages/home.vue b/pages/home.vue new file mode 100644 index 0000000..24e22a1 --- /dev/null +++ b/pages/home.vue @@ -0,0 +1,44 @@ + + + \ No newline at end of file From 26bf8352d95faf50c1c03a898cd10c3f9b609dab Mon Sep 17 00:00:00 2001 From: Lukas Lanzner Date: Wed, 4 Dec 2024 13:20:09 +0100 Subject: [PATCH 20/33] feat(friends): Add endpoint to accept and decline friend requests. --- server/api/v1/user/friends/index.get.ts | 2 +- server/api/v1/user/friends/index.post.ts | 2 +- server/api/v1/user/friends/index.put.ts | 54 +++++++++++++++++++++++- types/api/user.friends.ts | 34 +++++++++++---- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/server/api/v1/user/friends/index.get.ts b/server/api/v1/user/friends/index.get.ts index 9356818..9942fd0 100644 --- a/server/api/v1/user/friends/index.get.ts +++ b/server/api/v1/user/friends/index.get.ts @@ -17,7 +17,7 @@ export default defineEventHandler(async (event) => { const param: GetFriendParam = {user_id: user.id}; const {data, error}: { - data: GetFriendsResponse | null, + data: GetFriendsResponse[] | null, error: FriendError | null } = await client.rpc('get_friends', param as never); diff --git a/server/api/v1/user/friends/index.post.ts b/server/api/v1/user/friends/index.post.ts index fe7bed2..5c373bc 100644 --- a/server/api/v1/user/friends/index.post.ts +++ b/server/api/v1/user/friends/index.post.ts @@ -5,7 +5,7 @@ import type {SendFriendRequestParam} from "~/types/api/user.friends"; const userSchema = z.object({ receiver_id: z.string().uuid() -}) +}).readonly() export default defineEventHandler(async (event) => { // validate post-request body diff --git a/server/api/v1/user/friends/index.put.ts b/server/api/v1/user/friends/index.put.ts index d5fe53c..f07f455 100644 --- a/server/api/v1/user/friends/index.put.ts +++ b/server/api/v1/user/friends/index.put.ts @@ -1 +1,53 @@ -// accept or decline friend request \ No newline at end of file +import {z} from "zod"; +import {FriendAction} from "~/types/api/user.friends"; +import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server"; +import type {FriendActionParam} from "~/types/api/user.friends"; +import type {PostgrestError} from "@supabase/postgrest-js"; + +const friendActionSchema = z.object({ + friendship_id: z.bigint().positive(), + action: z.nativeEnum(FriendAction) +}).readonly() + +export default defineEventHandler(async (event) => { + // validate post-request body + const result = await readValidatedBody(event, body => friendActionSchema.safeParse(body)) + if (!result.success) { + setResponseStatus(event, 400); + return {error: result.error.issues}; + } + + // Require user to be authenticated + const user = await serverSupabaseUser(event); + if (!user?.id) { + setResponseStatus(event, 401); + return {error: 'unauthenticated'}; + } + + // Send request + const client = serverSupabaseServiceRole(event); + const param: FriendActionParam = {friendship_id: result.data.friendship_id}; + + let error: PostgrestError | null = null; + switch (result.data.action) { + case FriendAction.ACCEPT: { + const {error: acceptErr} = await client.rpc('accept_friend_request', param as never); + error = acceptErr; + error = acceptErr; + break; + } + case FriendAction.DECLINE: { + const {error: declineErr} = await client.rpc('decline_friend_request', param as never); + error = declineErr; + break; + } + } + + // Handle errors + if (error) { + setResponseStatus(event, 500); + return {error: error.message}; + } + + return {}; +}); \ No newline at end of file diff --git a/types/api/user.friends.ts b/types/api/user.friends.ts index 9bfb869..a51ee65 100644 --- a/types/api/user.friends.ts +++ b/types/api/user.friends.ts @@ -1,11 +1,4 @@ -export interface GetFriendsResponse { - friendship_id: bigint, - friend_id: string, - created_at: string - updated_at: string - status: "pending" | "accepted" -} - +// ----- Internal Types ----- export interface FriendError { message: string } @@ -17,4 +10,27 @@ export interface GetFriendParam { export interface SendFriendRequestParam { sender_id: string, receiver_id: string -} \ No newline at end of file +} + +export interface FriendActionParam { + friendship_id: bigint, +} + +// ----- For API Consumer ----- +export interface GetFriendsResponse { + friendship_id: bigint, + friend_id: string, + created_at: string + updated_at: string + status: "pending" | "accepted" +} + +export enum FriendAction { + ACCEPT = "accept", + DECLINE = "decline" +} + +export interface FriendActionRequest { + action: FriendAction, + friendship_id: bigint +} From cd4da148fefb9b0040454ee74a2d5f3f2c4dbf87 Mon Sep 17 00:00:00 2001 From: synan798 Date: Wed, 4 Dec 2024 13:42:38 +0100 Subject: [PATCH 21/33] added game controls and user/opponent view to homepage --- components/home/Controls/GameButtons.vue | 15 ++++++++ components/home/Turns/Opponent.vue | 14 ++++++++ components/home/Turns/User.vue | 14 ++++++++ components/home/Users/UserBox.vue | 25 ++++++++++++++ pages/home.vue | 44 ------------------------ pages/index.vue | 7 ++++ 6 files changed, 75 insertions(+), 44 deletions(-) create mode 100644 components/home/Controls/GameButtons.vue create mode 100644 components/home/Turns/Opponent.vue create mode 100644 components/home/Turns/User.vue create mode 100644 components/home/Users/UserBox.vue delete mode 100644 pages/home.vue diff --git a/components/home/Controls/GameButtons.vue b/components/home/Controls/GameButtons.vue new file mode 100644 index 0000000..36344c6 --- /dev/null +++ b/components/home/Controls/GameButtons.vue @@ -0,0 +1,15 @@ + + + + diff --git a/components/home/Turns/Opponent.vue b/components/home/Turns/Opponent.vue new file mode 100644 index 0000000..611abb5 --- /dev/null +++ b/components/home/Turns/Opponent.vue @@ -0,0 +1,14 @@ + + + diff --git a/components/home/Turns/User.vue b/components/home/Turns/User.vue new file mode 100644 index 0000000..326112b --- /dev/null +++ b/components/home/Turns/User.vue @@ -0,0 +1,14 @@ + + + diff --git a/components/home/Users/UserBox.vue b/components/home/Users/UserBox.vue new file mode 100644 index 0000000..60e7176 --- /dev/null +++ b/components/home/Users/UserBox.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/pages/home.vue b/pages/home.vue deleted file mode 100644 index 24e22a1..0000000 --- a/pages/home.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue index 8686f48..e7f44be 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -24,6 +24,13 @@ useSeoMeta({ 3C
+ \ No newline at end of file diff --git a/components/home/Turns/User.vue b/components/home/Turns/User.vue index 326112b..824cbb5 100644 --- a/components/home/Turns/User.vue +++ b/components/home/Turns/User.vue @@ -6,9 +6,9 @@

Your Turn

- - - + + +
diff --git a/components/home/Users/UserBox.vue b/components/home/Users/UserBox.vue index 60e7176..1355cf6 100644 --- a/components/home/Users/UserBox.vue +++ b/components/home/Users/UserBox.vue @@ -17,9 +17,10 @@ \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue index e7f44be..65289b0 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -26,9 +26,9 @@ useSeoMeta({ diff --git a/server/api/v1/playlist/[uid].get.ts b/server/api/v1/playlist/[uid].get.ts deleted file mode 100644 index b9be938..0000000 --- a/server/api/v1/playlist/[uid].get.ts +++ /dev/null @@ -1,36 +0,0 @@ -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('*').eq('id', playlistId).single(); - - if (error) { - setResponseStatus(event, 500); - return {error: error.message}; - } - - return data; -}) \ No newline at end of file diff --git a/server/api/v1/playlist/index.get.ts b/server/api/v1/playlist/index.get.ts deleted file mode 100644 index 1a23cb1..0000000 --- a/server/api/v1/playlist/index.get.ts +++ /dev/null @@ -1,27 +0,0 @@ -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(); - - if (error) { - setResponseStatus(event, 500); - return {error: error.message}; - } - - return data; -}) \ No newline at end of file diff --git a/server/api/v1/playlist/index.post.ts b/server/api/v1/playlist/index.post.ts deleted file mode 100644 index 7fa2412..0000000 --- a/server/api/v1/playlist/index.post.ts +++ /dev/null @@ -1,55 +0,0 @@ -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 client = serverSupabaseServiceRole(event) - const {data, error} = await client.from('playlists').insert(playlistInsert as never).select().single(); //todo: fix type error! - - if (error) { - setResponseStatus(event, 400); - if (error.code === UNIQUE_VIOLATION) - return {error: 'Playlist with this ID already exists'}; - setResponseStatus(event, 500); - return {error: error.message}; - } - - - setResponseStatus(event, 201); - return data; -}) \ No newline at end of file diff --git a/server/api/v1/playlist/playlist.http b/server/api/v1/playlist/playlist.http deleted file mode 100644 index 629a587..0000000 --- a/server/api/v1/playlist/playlist.http +++ /dev/null @@ -1,36 +0,0 @@ -@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 -} - From bec55ff7d0f01f956c564a3b8a3664c484384ed3 Mon Sep 17 00:00:00 2001 From: synan798 Date: Thu, 5 Dec 2024 08:57:45 +0100 Subject: [PATCH 33/33] fixef ESLint errors --- components/profile/Friendlist.vue | 5 ++--- components/profile/ProfileInformation.vue | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/components/profile/Friendlist.vue b/components/profile/Friendlist.vue index 177a354..dac4093 100644 --- a/components/profile/Friendlist.vue +++ b/components/profile/Friendlist.vue @@ -1,7 +1,5 @@