diff --git a/bun.lock b/bun.lock index 9d1ad38..af5d8c8 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,9 @@ "@aws-sdk/client-s3": "^3.1036.0", "@better-auth/drizzle-adapter": "^1.6.9", "@better-auth/passkey": "^1.6.9", + "@elysia/cron": "^1.4.2", "@elysia/eden": "^1.4.10", + "@elysia/openapi": "^1.4.15", "@serwist/expiration": "^9.5.11", "@serwist/next": "^9.5.11", "@serwist/precaching": "^9.5.11", @@ -210,8 +212,12 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@elysia/cron": ["@elysia/cron@1.4.2", "", { "dependencies": { "croner": "^6.0.3" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-YCaFCaT7CO7Lmfv66YGCqMYaAEjLZBpPtocaj9kS6NkhXqayhyKxqUOLSVW3qpIL9ZAE9m47GC650ZtvHpA58g=="], + "@elysia/eden": ["@elysia/eden@1.4.10", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-vcZXQcW6wZj6rhTxaiTkuCbVuS/yAJQ9jqCM6b83a5hu99F5Aj3HOyWXL17iod2Rz+pJpYsQGX0XaeoCsMpw2g=="], + "@elysia/openapi": ["@elysia/openapi@1.4.15", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-MrVGCPo5hnRF4/yWGOyH5ddjt4DlSIbeC5rrkupQtHY8TEQe+XdJa2YM9aUOrtgtRcy4UNGyiVlLl/jllPkPQA=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -904,6 +910,8 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "croner": ["croner@6.0.7", "", {}, "sha512-k3Xx3Rcclfr60Yx4TmvsF3Yscuiql8LSvYLaphTsaq5Hk8La4Z/udmUANMOTKpgGGroI2F6/XOr9cU9OFkYluQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], diff --git a/lib/api/admin-comments.ts b/lib/api/admin-comments.ts index c6f2266..f9695e2 100644 --- a/lib/api/admin-comments.ts +++ b/lib/api/admin-comments.ts @@ -11,7 +11,7 @@ import { requireAdmin, } from "@/lib/auth/guard" -export const adminCommentRoutes = new Elysia({ prefix: "/admin" }) +export const adminCommentRoutes = new Elysia({ prefix: "/admin", detail: { tags: ["Admin"] } }) .get( "/comments", async ({ query, request, set }) => { diff --git a/lib/api/admin-performance.ts b/lib/api/admin-performance.ts index 30770b7..a7b835d 100644 --- a/lib/api/admin-performance.ts +++ b/lib/api/admin-performance.ts @@ -14,7 +14,7 @@ import { requireAdmin, } from "@/lib/auth/guard" -export const adminPerformanceRoutes = new Elysia({ prefix: "/admin" }) +export const adminPerformanceRoutes = new Elysia({ prefix: "/admin", detail: { tags: ["Admin"] } }) .get( "/performance", async ({ query, request, set }) => { diff --git a/lib/api/admin-reports.ts b/lib/api/admin-reports.ts index 85d675b..f8dd67c 100644 --- a/lib/api/admin-reports.ts +++ b/lib/api/admin-reports.ts @@ -10,7 +10,7 @@ import { import { eq, desc, sql, and, inArray } from "drizzle-orm" import { requireContributorOrAdmin } from "@/lib/auth/guard" -export const adminReportRoutes = new Elysia({ prefix: "/admin" }) +export const adminReportRoutes = new Elysia({ prefix: "/admin", detail: { tags: ["Admin"] } }) .get( "/reports", async ({ query, request, set }) => { diff --git a/lib/api/admin-storage.ts b/lib/api/admin-storage.ts index b65d353..d552a4d 100644 --- a/lib/api/admin-storage.ts +++ b/lib/api/admin-storage.ts @@ -5,7 +5,7 @@ import { eq, sql, and, ilike, desc } from "drizzle-orm" import { requireAdmin } from "@/lib/auth/guard" import { deleteObject, isR2Configured } from "@/lib/storage" -export const adminStorageRoutes = new Elysia({ prefix: "/admin/storage" }) +export const adminStorageRoutes = new Elysia({ prefix: "/admin/storage", detail: { tags: ["Admin"] } }) // ── GET /stats ────────────────────────────────────────────────── .get( diff --git a/lib/api/app.ts b/lib/api/app.ts index 927c353..ebca55f 100644 --- a/lib/api/app.ts +++ b/lib/api/app.ts @@ -1,6 +1,9 @@ import { Elysia } from "elysia" +import { openapi } from "@elysia/openapi" +import { cron, Patterns } from "@elysia/cron" import { auth } from "@/lib/auth" import { rateLimit } from "@/lib/auth/rate-limit" +import { taskRegistry } from "./cron" import { healthRoutes, userRoutes, @@ -59,6 +62,76 @@ const betterAuth = new Elysia({ name: "better-auth" }) }) export const app = new Elysia({ prefix: "/api" }) + .use( + openapi({ + path: "/openapi", + documentation: { + info: { + title: "DeckyVault API", + version: "2026.0.100", + description: + "API for DeckyVault — Steam Deck game compatibility, performance reports, and community features.", + }, + tags: [ + { name: "Health", description: "Health check endpoints" }, + { name: "Auth", description: "Authentication endpoints" }, + { name: "Users", description: "User management" }, + { name: "Games", description: "Games listing and details" }, + { name: "Hardware", description: "Hardware submission and stats" }, + { name: "Performance", description: "Performance reports and verification" }, + { name: "Comments", description: "Game comments" }, + { name: "Reports", description: "User reports" }, + { name: "Admin", description: "Admin-only endpoints" }, + { name: "Steam", description: "Steam proxy endpoints" }, + { name: "Search", description: "Search endpoints" }, + { name: "Contact", description: "Contact form" }, + { name: "Dashboard", description: "Dashboard data" }, + { name: "Cron", description: "Scheduled job triggers" }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + }, + }), + ) + .use( + cron({ + name: "orphan_detection", + pattern: Patterns.EVERY_DAY_AT_2AM, + async run() { + const task = taskRegistry.get("orphan_detection") + if (!task) return + try { + const result = await task() + console.log("[cron] orphan_detection:", result.status, result.details) + } catch (err) { + console.error("[cron] orphan_detection failed:", err) + } + }, + }), + ) + .use( + cron({ + name: "storage_cleanup", + pattern: Patterns.EVERY_DAY_AT_3AM, + async run() { + const task = taskRegistry.get("storage_cleanup") + if (!task) return + try { + const result = await task() + console.log("[cron] storage_cleanup:", result.status, result.details) + } catch (err) { + console.error("[cron] storage_cleanup failed:", err) + } + }, + }), + ) .onError(({ code, error, set, request }) => { console.error( `[API Error] ${code} ${request.url}`, diff --git a/lib/api/comments.ts b/lib/api/comments.ts index c776720..732bc0a 100644 --- a/lib/api/comments.ts +++ b/lib/api/comments.ts @@ -6,6 +6,7 @@ import { requireRole } from "@/lib/auth/guard" export const commentsRoutes = new Elysia({ prefix: "/games/:gameId/comments", + detail: { tags: ["Comments"] }, }) // LIST top-level comments for a game (paginated) .get( diff --git a/lib/api/community-suggestions.ts b/lib/api/community-suggestions.ts index 1040d0d..76b3002 100644 --- a/lib/api/community-suggestions.ts +++ b/lib/api/community-suggestions.ts @@ -58,6 +58,7 @@ const allowedFields = [ export const communitySuggestionRoutes = new Elysia({ prefix: "/community-suggestions", + detail: { tags: ["Games"] }, }) // Admin list with pagination and filtering diff --git a/lib/api/compare.ts b/lib/api/compare.ts index b85c373..b835f4c 100644 --- a/lib/api/compare.ts +++ b/lib/api/compare.ts @@ -3,7 +3,7 @@ import { db } from "@/lib/db/index" import { games, gameVersions, performanceEntries, hardware } from "@/lib/db/schema" import { eq, and, inArray } from "drizzle-orm" -export const compareRoutes = new Elysia({ prefix: "/compare" }) +export const compareRoutes = new Elysia({ prefix: "/compare", detail: { tags: ["Games"] } }) .get( "/games", async ({ query, set }) => { diff --git a/lib/api/contact.ts b/lib/api/contact.ts index 6c96765..9ddf494 100644 --- a/lib/api/contact.ts +++ b/lib/api/contact.ts @@ -58,7 +58,7 @@ const CATEGORY_LABELS: Record = { const VALID_CATEGORIES = ["bug", "game_data", "user_report", "feature", "feedback", "database"] -export const contactRoutes = new Elysia({ prefix: "/contact" }).post( +export const contactRoutes = new Elysia({ prefix: "/contact", detail: { tags: ["Contact"] } }).post( "/", async ({ body, request, set }) => { const payload = body as { diff --git a/lib/api/cron.ts b/lib/api/cron.ts index 8334329..f91ac7b 100644 --- a/lib/api/cron.ts +++ b/lib/api/cron.ts @@ -15,7 +15,7 @@ interface CronTaskResult { // ── Task Registry ─────────────────────────────────────────────────── type CronTask = () => Promise -const taskRegistry = new Map() +export const taskRegistry = new Map() export function registerCronTask(name: string, task: CronTask): void { taskRegistry.set(name, task) @@ -111,7 +111,7 @@ registerCronTask("orphan_detection", async () => { }) // ── Cron Route ────────────────────────────────────────────────────── -export const cronRoutes = new Elysia({ prefix: "/cron" }).post( +export const cronRoutes = new Elysia({ prefix: "/cron", detail: { tags: ["Cron"] } }).post( "/daily", async ({ query, set }) => { const cronSecret = process.env.CRON_SECRET diff --git a/lib/api/crud-builder.ts b/lib/api/crud-builder.ts index ef92b46..ca595fd 100644 --- a/lib/api/crud-builder.ts +++ b/lib/api/crud-builder.ts @@ -60,6 +60,7 @@ export function createCrudRoutes( primaryKey?: string paramName?: string softDelete?: boolean + tags?: string[] }, ) { const { @@ -71,6 +72,7 @@ export function createCrudRoutes( primaryKey = "id", paramName = primaryKey, softDelete = false, + tags, } = config const columns = getTableColumns(table) as Record @@ -80,7 +82,10 @@ export function createCrudRoutes( throw new Error(`Primary key column "${primaryKey}" not found on table`) } - const routes = new Elysia({ prefix }) + const routes = new Elysia({ + prefix, + ...(tags ? { detail: { tags } } : {}), + }) // ── LIST ────────────────────────────────────────────────────────── routes.get( @@ -157,6 +162,10 @@ export function createCrudRoutes( // Dynamic filter fields are too varied for static TypeBox, // so we allow any string keys with filter_ prefix }), + detail: { + summary: `List ${name}s`, + description: `Returns a paginated list of ${name}s with optional search and filtering.`, + }, }, ) @@ -183,6 +192,10 @@ export function createCrudRoutes( params: t.Object({ [paramName]: t.String(), }), + detail: { + summary: `Get ${name} by ID`, + description: `Returns a single ${name} by its unique identifier.`, + }, }, ) @@ -211,6 +224,10 @@ export function createCrudRoutes( }, { body: t.Record(t.String(), t.Any()), + detail: { + summary: `Create ${name}`, + description: `Creates a new ${name}. Requires authentication.`, + }, }, ) @@ -256,6 +273,10 @@ export function createCrudRoutes( [paramName]: t.String(), }), body: t.Record(t.String(), t.Any()), + detail: { + summary: `Update ${name}`, + description: `Updates an existing ${name} by ID. Requires authentication.`, + }, }, ) @@ -303,6 +324,10 @@ export function createCrudRoutes( params: t.Object({ [paramName]: t.String(), }), + detail: { + summary: `Delete ${name}`, + description: `Deletes a ${name} by ID. Requires admin or contributor role.`, + }, }, ) diff --git a/lib/api/dashboard-public.ts b/lib/api/dashboard-public.ts index addf7c1..823d686 100644 --- a/lib/api/dashboard-public.ts +++ b/lib/api/dashboard-public.ts @@ -4,7 +4,7 @@ import { sql } from "drizzle-orm" const SEVEN_DAYS_AGO = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) -export const dashboardPublicRoutes = new Elysia({ prefix: "/dashboard" }) +export const dashboardPublicRoutes = new Elysia({ prefix: "/dashboard", detail: { tags: ["Dashboard"] } }) // ── Trending Games (7-day activity) ──────────────────────────────── .get( diff --git a/lib/api/dashboard.ts b/lib/api/dashboard.ts index 18772ee..c4b9464 100644 --- a/lib/api/dashboard.ts +++ b/lib/api/dashboard.ts @@ -10,7 +10,7 @@ import { import { eq, count, sql, gte, and, desc } from "drizzle-orm"; import { requireContributorOrAdmin } from "@/lib/auth/guard"; -export const dashboardRoutes = new Elysia({ prefix: "/dashboard" }).get( +export const dashboardRoutes = new Elysia({ prefix: "/dashboard", detail: { tags: ["Dashboard"] } }).get( "/stats", async ({ request, set }) => { const guard = await requireContributorOrAdmin(request.headers); diff --git a/lib/api/game-stats.ts b/lib/api/game-stats.ts index 19df33c..95fca43 100644 --- a/lib/api/game-stats.ts +++ b/lib/api/game-stats.ts @@ -8,7 +8,7 @@ import { } from "@/lib/db/schema" import { eq, and, inArray, sql } from "drizzle-orm" -export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( +export const gameStatsRoutes = new Elysia({ prefix: "/games", detail: { tags: ["Games"] } }).get( "/:gameId/stats", async ({ params, set }) => { const { gameId } = params diff --git a/lib/api/game-stub.ts b/lib/api/game-stub.ts index 8030e35..342c02b 100644 --- a/lib/api/game-stub.ts +++ b/lib/api/game-stub.ts @@ -1,7 +1,7 @@ import { Elysia, t } from "elysia" import { ensureSteamGame } from "@/lib/steam/sync" -export const gameStubRoutes = new Elysia({ prefix: "/games" }).post( +export const gameStubRoutes = new Elysia({ prefix: "/games", detail: { tags: ["Games"] } }).post( "/stub", async ({ body, set }) => { const result = await ensureSteamGame(body.steamAppId) diff --git a/lib/api/games-listing.ts b/lib/api/games-listing.ts index 77c98a5..ada3a7b 100644 --- a/lib/api/games-listing.ts +++ b/lib/api/games-listing.ts @@ -13,7 +13,7 @@ import { fuzzySearchTerm } from "@/lib/db/search" const MAX_OFFSET = 10000 const PAGE_SIZE = 24 -export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get( +export const gamesListingRoutes = new Elysia({ prefix: "/games/listing", detail: { tags: ["Games"] } }).get( "/", async ({ query }) => { const offset = Math.min(Number(query.offset) || 0, MAX_OFFSET) diff --git a/lib/api/games-manual.ts b/lib/api/games-manual.ts index a276a1b..900daf2 100644 --- a/lib/api/games-manual.ts +++ b/lib/api/games-manual.ts @@ -4,7 +4,7 @@ import { games, gameVersions, gamePlatformSupport } from "@/lib/db/schema" import { ilike, eq } from "drizzle-orm" import { requireRole } from "@/lib/auth/guard" -export const gamesManualRoutes = new Elysia({ prefix: "/games" }) +export const gamesManualRoutes = new Elysia({ prefix: "/games", detail: { tags: ["Games"] } }) .post( "/manual", async ({ request, body, set }) => { diff --git a/lib/api/games.ts b/lib/api/games.ts index e0ca5cf..56f8ffd 100644 --- a/lib/api/games.ts +++ b/lib/api/games.ts @@ -10,6 +10,7 @@ import { requireRole } from "@/lib/auth/guard" export const gamesRoutes = createCrudRoutes(games, { prefix: "/games", name: "Game", + tags: ["Games"], auth: { read: "public", write: "contributor", delete: "admin" }, search: { fields: ["title", "developer", "publisher"] }, filter: { fields: ["source", "onlineMultiplayerStatus", "syncStatus"] }, @@ -17,7 +18,10 @@ export const gamesRoutes = createCrudRoutes(games, { }) // ── Game Versions (nested under /games/:gameId/versions) ────────── -export const gameVersionsRoutes = new Elysia({ prefix: "/games/:gameId/versions" }) +export const gameVersionsRoutes = new Elysia({ + prefix: "/games/:gameId/versions", + detail: { tags: ["Games"] }, +}) // LIST versions for a game .get( "/", @@ -192,7 +196,10 @@ export const gameVersionsRoutes = new Elysia({ prefix: "/games/:gameId/versions" const MAX_BULK_SYNC = 1000 -export const gameSyncRoutes = new Elysia({ prefix: "/games" }) +export const gameSyncRoutes = new Elysia({ + prefix: "/games", + detail: { tags: ["Admin"] }, +}) // Bulk sync with streaming progress (defined before /:gameId/sync to avoid route conflict) .post( "/sync/bulk", diff --git a/lib/api/hardware-stats.ts b/lib/api/hardware-stats.ts index c066cca..e26b208 100644 --- a/lib/api/hardware-stats.ts +++ b/lib/api/hardware-stats.ts @@ -8,7 +8,7 @@ import { } from "@/lib/db/schema" import { eq, and, sql, desc } from "drizzle-orm" -export const hardwareStatsRoutes = new Elysia({ prefix: "/hardware" }) +export const hardwareStatsRoutes = new Elysia({ prefix: "/hardware", detail: { tags: ["Hardware"] } }) // ── All devices with aggregated stats ────────────────────── .get( "/stats", diff --git a/lib/api/hardware.ts b/lib/api/hardware.ts index 895daf2..01b23e8 100644 --- a/lib/api/hardware.ts +++ b/lib/api/hardware.ts @@ -4,6 +4,7 @@ import { hardware } from "@/lib/db/schema" export const hardwareRoutes = createCrudRoutes(hardware, { prefix: "/hardware", name: "Hardware", + tags: ["Hardware"], primaryKey: "slug", auth: { read: "public", write: "admin", delete: "admin" }, filter: { fields: ["deviceType"] }, diff --git a/lib/api/health.ts b/lib/api/health.ts index f9918e3..fc313cf 100644 --- a/lib/api/health.ts +++ b/lib/api/health.ts @@ -1,10 +1,19 @@ import { Elysia } from "elysia" -export const healthRoutes = new Elysia({ prefix: "/health" }).get( +export const healthRoutes = new Elysia({ + prefix: "/health", + detail: { tags: ["Health"] }, +}).get( "/", () => ({ status: "ok", timestamp: new Date().toISOString(), service: "deckyvault-api", }), + { + detail: { + summary: "Health check", + description: "Returns the current health status of the API.", + }, + }, ) diff --git a/lib/api/performance-submit.ts b/lib/api/performance-submit.ts index c188d7c..ad7c8fa 100644 --- a/lib/api/performance-submit.ts +++ b/lib/api/performance-submit.ts @@ -18,7 +18,7 @@ import { processScreenshot, isAllowedMimeType, validateMagicBytes } from "@/lib/ const MAX_SCREENSHOTS_PER_ENTRY = 2 const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 // 10 MB -export const performanceSubmitRoutes = new Elysia({ prefix: "/performance" }) +export const performanceSubmitRoutes = new Elysia({ prefix: "/performance", detail: { tags: ["Performance"] } }) .get( "/autocomplete", async ({ query }) => { diff --git a/lib/api/performance.ts b/lib/api/performance.ts index 2727e1a..c323ceb 100644 --- a/lib/api/performance.ts +++ b/lib/api/performance.ts @@ -15,6 +15,7 @@ const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 // 10 MB export const performanceRoutes = createCrudRoutes(performanceEntries, { prefix: "/performance", name: "Performance Entry", + tags: ["Performance"], auth: { read: "public", write: "user", delete: "admin" }, softDelete: true, search: { fields: ["userNotes"] }, @@ -24,6 +25,7 @@ export const performanceRoutes = createCrudRoutes(performanceEntries, { // ── Verify endpoint (admin/mod) ─────────────────────────────────── export const performanceVerifyRoutes = new Elysia({ prefix: "/performance", + detail: { tags: ["Performance"] }, }) .post( "/:id/verify", diff --git a/lib/api/playability.ts b/lib/api/playability.ts index 742fc89..32d2635 100644 --- a/lib/api/playability.ts +++ b/lib/api/playability.ts @@ -141,7 +141,7 @@ export async function recalculatePlayability(gameId: string): Promise<{ return { gamePlayability: worstStatus, deviceResults: results }; } -export const playabilityRoutes = new Elysia({ prefix: "/playability" }) +export const playabilityRoutes = new Elysia({ prefix: "/playability", detail: { tags: ["Games"] } }) // Manual trigger for recalculation (admin/contributor) .post("/calculate/:gameId", async ({ params, request, set }) => { const guard = await requireContributorOrAdmin(request.headers); diff --git a/lib/api/profile-photo.ts b/lib/api/profile-photo.ts index a83aec6..e460252 100644 --- a/lib/api/profile-photo.ts +++ b/lib/api/profile-photo.ts @@ -29,7 +29,7 @@ function validateMagicBytes(buffer: Buffer, declaredMime: string): boolean { return expected.every((byte, i) => buffer[i] === byte) } -export const profilePhotoRoutes = new Elysia({ prefix: "/user" }) +export const profilePhotoRoutes = new Elysia({ prefix: "/user", detail: { tags: ["Users"] } }) // ── Upload Profile Photo ────────────────────────────────────────── .post( diff --git a/lib/api/reports.ts b/lib/api/reports.ts index 137f177..1c94f3d 100644 --- a/lib/api/reports.ts +++ b/lib/api/reports.ts @@ -4,7 +4,7 @@ import { reports } from "@/lib/db/schema" import { eq, and } from "drizzle-orm" import { requireRole } from "@/lib/auth/guard" -export const reportRoutes = new Elysia({ prefix: "/performance" }).post( +export const reportRoutes = new Elysia({ prefix: "/performance", detail: { tags: ["Reports"] } }).post( "/:id/report", async ({ params, body, request, set }) => { const guard = await requireRole(request.headers, [ diff --git a/lib/api/saved-filters.ts b/lib/api/saved-filters.ts index 7d480a2..f101c7b 100644 --- a/lib/api/saved-filters.ts +++ b/lib/api/saved-filters.ts @@ -4,7 +4,7 @@ import { savedFilters } from "@/lib/db/schema" import { eq, and, desc } from "drizzle-orm" import { requireAuth } from "@/lib/auth/guard" -export const savedFilterRoutes = new Elysia({ prefix: "/saved-filters" }) +export const savedFilterRoutes = new Elysia({ prefix: "/saved-filters", detail: { tags: ["Games"] } }) // Get user's saved filters .get("/", async ({ request, set }) => { diff --git a/lib/api/saved-games.ts b/lib/api/saved-games.ts index dfde3b2..18d79ba 100644 --- a/lib/api/saved-games.ts +++ b/lib/api/saved-games.ts @@ -4,7 +4,7 @@ import { db } from "@/lib/db/index" import { savedGames, games } from "@/lib/db/schema" import { eq, and, sql } from "drizzle-orm" -export const savedGamesRoutes = new Elysia({ prefix: "/user/me/saved-games" }) +export const savedGamesRoutes = new Elysia({ prefix: "/user/me/saved-games", detail: { tags: ["Games"] } }) .post( "/", async ({ request, body, set }) => { diff --git a/lib/api/screenshots.ts b/lib/api/screenshots.ts index 6c8c5b4..7a597b8 100644 --- a/lib/api/screenshots.ts +++ b/lib/api/screenshots.ts @@ -9,7 +9,7 @@ import { processScreenshot, isAllowedMimeType } from "@/lib/image-processing" const MAX_SCREENSHOTS_PER_ENTRY = 2 const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 // 10 MB raw -export const screenshotRoutes = new Elysia({ prefix: "/performance" }) +export const screenshotRoutes = new Elysia({ prefix: "/performance", detail: { tags: ["Performance"] } }) // ── Upload screenshots ───────────────────────────────────────── .post( diff --git a/lib/api/search-unified.ts b/lib/api/search-unified.ts index 477ce62..0d30fc9 100644 --- a/lib/api/search-unified.ts +++ b/lib/api/search-unified.ts @@ -24,7 +24,7 @@ interface SteamSearchResponse { total: number } -export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get( +export const searchUnifiedRoutes = new Elysia({ prefix: "/search", detail: { tags: ["Search"] } }).get( "/unified", async ({ query, set }) => { if (!query.q || query.q.length < 2) { diff --git a/lib/api/steam-reviews.ts b/lib/api/steam-reviews.ts index 212717f..cae3fb9 100644 --- a/lib/api/steam-reviews.ts +++ b/lib/api/steam-reviews.ts @@ -47,7 +47,7 @@ interface SteamReviewResponse { reviews: SteamReview[] } -export const steamReviewRoutes = new Elysia({ prefix: "/steam-reviews" }) +export const steamReviewRoutes = new Elysia({ prefix: "/steam-reviews", detail: { tags: ["Steam"] } }) // Get embedded Steam reviews for a game .get( diff --git a/lib/api/steam-search.ts b/lib/api/steam-search.ts index aa5edb0..3476db5 100644 --- a/lib/api/steam-search.ts +++ b/lib/api/steam-search.ts @@ -12,7 +12,7 @@ interface SteamSearchItem { } } -export const steamSearchRoutes = new Elysia({ prefix: "/search" }) +export const steamSearchRoutes = new Elysia({ prefix: "/search", detail: { tags: ["Search"] } }) .get( "/steam", async ({ query, set }) => { diff --git a/lib/api/steamgrid-proxy.ts b/lib/api/steamgrid-proxy.ts index 70bc08b..398502d 100644 --- a/lib/api/steamgrid-proxy.ts +++ b/lib/api/steamgrid-proxy.ts @@ -2,7 +2,7 @@ import { Elysia, t } from "elysia" const STEAMGRIDDB_BASE = "https://www.steamgriddb.com/api/v2" -export const steamgridProxyRoutes = new Elysia({ prefix: "/steamgrid" }) +export const steamgridProxyRoutes = new Elysia({ prefix: "/steamgrid", detail: { tags: ["Steam"] } }) .get( "/search", async ({ query, set }) => { diff --git a/lib/api/user.ts b/lib/api/user.ts index 40fc908..a56d9f7 100644 --- a/lib/api/user.ts +++ b/lib/api/user.ts @@ -5,7 +5,7 @@ import { user, performanceEntries, games, gameVersions, hardware, account, passk import { eq, sql, and, desc } from "drizzle-orm" import { hashPassword } from "better-auth/crypto" -export const userRoutes = new Elysia({ prefix: "/user" }) +export const userRoutes = new Elysia({ prefix: "/user", detail: { tags: ["Users"] } }) .get( "/profile/:id", async ({ params, set }) => { diff --git a/next.config.ts b/next.config.ts index 8ea6efb..81d61ae 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,7 +7,7 @@ const nextConfig: NextConfig = { NEXT_PUBLIC_APP_VERSION: version, }, // Elysia is Bun-native and must not be bundled by Next.js - serverExternalPackages: ["elysia", "@elysiajs/eden"], + serverExternalPackages: ["elysia", "@elysiajs/eden", "@elysia/openapi", "@elysia/cron"], images: { remotePatterns: [ { diff --git a/package.json b/package.json index 3d12c1c..a94ff49 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "@aws-sdk/client-s3": "^3.1036.0", "@better-auth/drizzle-adapter": "^1.6.9", "@better-auth/passkey": "^1.6.9", + "@elysia/cron": "^1.4.2", "@elysia/eden": "^1.4.10", + "@elysia/openapi": "^1.4.15", "@serwist/expiration": "^9.5.11", "@serwist/next": "^9.5.11", "@serwist/precaching": "^9.5.11",