diff --git a/bun.lock b/bun.lock index 4d0cbbf..482c6f9 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "cvsa", @@ -88,11 +87,11 @@ "@alikia/random-key": "^1.1.1", "@elysiajs/cors": "^1.4.0", "@elysiajs/jwt": "^1.4.0", - "@elysiajs/openapi": "^1.4.0", + "@elysiajs/openapi": "^1.4.11", "@elysiajs/server-timing": "^1.4.0", "@rabbit-company/argon2id": "^2.1.0", "chalk": "^5.6.2", - "elysia": "^1.4.0", + "elysia": "^1.4.15", "elysia-ip": "^1.0.10", "zod": "^4.1.12", }, @@ -415,7 +414,7 @@ "@elysiajs/jwt": ["@elysiajs/jwt@1.4.0", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Z0PvZhQxdDeKZ8HslXzDoXXD83NKExNPmoiAPki3nI2Xvh5wtUrBH+zWOD17yP14IbRo8fxGj3L25MRCAPsgPA=="], - "@elysiajs/openapi": ["@elysiajs/openapi@1.4.10", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-bEsETp/CGcs1CqH3zW6/CAI2g6d0K/g8wUuH7HwXQm0gtP18s9RnljJESuv4of3ePUoYQgy85t+dha+ABv+L/A=="], + "@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="], "@elysiajs/server-timing": ["@elysiajs/server-timing@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-vDFdHyi8Q43vgA5MaTQMA9v4/bgKrtqPrpVqVuHlMCRQgfOpvYGXPj3okSttyendG5r2bRHfyPG11lTWWIrzrQ=="], @@ -1537,7 +1536,7 @@ "electron-to-chromium": ["electron-to-chromium@1.5.222", "", {}, "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w=="], - "elysia": ["elysia@1.4.7", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "openapi-types": ">= 12.0.0" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-6dZjPO5wBBilZPzqZpuMq/bMiHn7jQ94sweiPTTR9MsofliZJcwcRB0XHAr7AvKblOlU72P+Cu8z6JNhZK8u/A=="], + "elysia": ["elysia@1.4.15", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-RaDqqZdLuC4UJetfVRQ4Z5aVpGgEtQ+pZnsbI4ZzEaf3l/MzuHcqSVoL/Fue3d6qE4RV9HMB2rAZaHyPIxkyzg=="], "elysia-api": ["elysia-api@workspace:packages/elysia"], @@ -2053,6 +2052,8 @@ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -2969,8 +2970,6 @@ "@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "@cvsa/backend/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], - "@deno/shim-deno/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -3285,8 +3284,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@cvsa/backend/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], - "@deno/shim-deno/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], diff --git a/packages/elysia/middlewares/auth.ts b/packages/elysia/middlewares/auth.ts new file mode 100644 index 0000000..ff2e332 --- /dev/null +++ b/packages/elysia/middlewares/auth.ts @@ -0,0 +1,74 @@ +import { Elysia } from "elysia"; +import { validateSession, User } from "@elysia/lib/auth"; + +export interface AuthenticatedContext { + user: User; + session: any; + isAuthenticated: boolean; +} + +/** + * Authentication middleware that validates session from cookie or authorization header + * + * This middleware: + * 1. Checks for sessionId in cookie (primary method) + * 2. Falls back to Authorization header with Bearer token + * 3. Validates the session using validateSession from lib/auth + * 4. Sets user and session context for authenticated routes + * 5. Returns 401 if authentication fails + */ +export const requireAuth = new Elysia({ name: "require-auth" }) + .derive(async ({ cookie, headers, set }) => { + let sessionId: string | null = null; + + // Try to get sessionId from cookie first + if (cookie.sessionId && typeof cookie.sessionId.value === "string") { + sessionId = cookie.sessionId.value; + } + // Fallback to Authorization header + else if (headers.authorization) { + const authHeader = headers.authorization; + if (authHeader.startsWith("Bearer ")) { + sessionId = authHeader.substring(7); + } + } + + // If no sessionId found, return unauthenticated + if (!sessionId) { + set.status = 401; + return { + user: null, + session: null, + isAuthenticated: false + }; + } + + // Validate the session + const validationResult = await validateSession(sessionId); + + if (!validationResult) { + set.status = 401; + return { + user: null, + session: null, + isAuthenticated: false + }; + } + + // Session is valid, return user and session context + return { + user: validationResult.user, + session: validationResult.session, + isAuthenticated: true + }; + }) + .onBeforeHandle({ as: "scoped" }, ({ user, status }) => { + if (!user) { + return status(401, { + message: "Authentication required." + }); + } + }) + .as("scoped"); + +export default requireAuth; diff --git a/packages/elysia/package.json b/packages/elysia/package.json index 37366c7..3acf854 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -10,11 +10,11 @@ "@alikia/random-key": "^1.1.1", "@elysiajs/cors": "^1.4.0", "@elysiajs/jwt": "^1.4.0", - "@elysiajs/openapi": "^1.4.0", + "@elysiajs/openapi": "^1.4.11", "@elysiajs/server-timing": "^1.4.0", "@rabbit-company/argon2id": "^2.1.0", "chalk": "^5.6.2", - "elysia": "^1.4.0", + "elysia": "^1.4.15", "elysia-ip": "^1.0.10", "zod": "^4.1.12" }, diff --git a/packages/elysia/routes/song/info.ts b/packages/elysia/routes/song/info.ts index 876b6d6..8336691 100644 --- a/packages/elysia/routes/song/info.ts +++ b/packages/elysia/routes/song/info.ts @@ -3,7 +3,7 @@ import { dbMain } from "@core/drizzle"; import { relations, singer, songs } from "@core/drizzle/main/schema"; import { eq, and } from "drizzle-orm"; import { bv2av } from "@elysia/lib/av_bv"; -import captchaMiddleware from "@elysia/middlewares/captcha"; +import { requireAuth } from "@elysia/middlewares/auth"; async function getSongIDFromBiliID(id: string) { let aid: number; @@ -113,56 +113,62 @@ const songInfoGetHandler = new Elysia({ prefix: "/song" }).get( } ); -const songInfoUpdateHandler = new Elysia({ prefix: "/song" }).use(captchaMiddleware).patch( - "/:id/info", - async ({ params, status, body }) => { - const id = params.id; - const songID = await getSongID(id); - if (!songID) { - return status(404, { - code: "SONG_NOT_FOUND", - message: "Given song cannot be found." - }); - } - const info = await getSongInfo(songID); - if (!info) { - return status(404, { - code: "SONG_NOT_FOUND", - message: "Given song cannot be found." - }); - } - if (body.name) { - await dbMain.update(songs).set({ name: body.name }).where(eq(songs.id, songID)); - } - if (body.producer) { - await dbMain - .update(songs) - .set({ producer: body.producer }) - .where(eq(songs.id, songID)) - .returning(); - } - const updatedData = await dbMain.select().from(songs).where(eq(songs.id, songID)); - return { - message: "Successfully updated song info.", - updated: updatedData.length > 0 ? updatedData[0] : null - }; - }, - { - response: { - 200: t.Object({ - message: t.String(), - updated: t.Any() - }), - 404: t.Object({ - message: t.String(), - code: t.String() - }) +const songInfoUpdateHandler = new Elysia({ prefix: "/song" }) + .use(requireAuth) + .patch( + "/:id/info", + async ({ params, status, body, user }) => { + const id = params.id; + const songID = await getSongID(id); + if (!songID) { + return status(404, { + code: "SONG_NOT_FOUND", + message: "Given song cannot be found." + }); + } + const info = await getSongInfo(songID); + if (!info) { + return status(404, { + code: "SONG_NOT_FOUND", + message: "Given song cannot be found." + }); + } + + if (body.name) { + await dbMain.update(songs).set({ name: body.name }).where(eq(songs.id, songID)); + } + if (body.producer) { + await dbMain + .update(songs) + .set({ producer: body.producer }) + .where(eq(songs.id, songID)) + .returning(); + } + const updatedData = await dbMain.select().from(songs).where(eq(songs.id, songID)); + return { + message: "Successfully updated song info.", + updated: updatedData.length > 0 ? updatedData[0] : null + }; }, - body: t.Object({ - name: t.Optional(t.String()), - producer: t.Optional(t.String()) - }) - } -); + { + response: { + 200: t.Object({ + message: t.String(), + updated: t.Any() + }), + 401: t.Object({ + message: t.String() + }), + 404: t.Object({ + message: t.String(), + code: t.String() + }) + }, + body: t.Object({ + name: t.Optional(t.String()), + producer: t.Optional(t.String()) + }) + } + ); export const songInfoHandler = new Elysia().use(songInfoGetHandler).use(songInfoUpdateHandler); diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index b8cdf4c..3f5babe 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -20,6 +20,15 @@ const app = new Elysia({ hostname: host } }) + .onError(({ code, status }) => { + if (code === "NOT_FOUND") + return status(404, { + message: "The requested resource was not found." + }); + return status(500, { + message: "An internal server error occurred." + }); + }) .use(onAfterHandler) .use(cors()) .use(openapi())