feat: add OpenAPI support and in-process cron scheduler
- Install @elysia/openapi to auto-generate API docs at /api/openapi - Install @elysia/cron for in-process scheduled jobs (orphan detection at 2am, storage cleanup at 3am) - Add Scalar UI docs with 14 tag groups and Bearer JWT security scheme - Add route-level detail annotations (tags, summaries) to all 30+ route files - Upgrade crud-builder to accept optional tags config - Keep HTTP cron endpoint as manual/admin fallback
This commit is contained in:
@@ -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=="],
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -58,6 +58,7 @@ const allowedFields = [
|
||||
|
||||
export const communitySuggestionRoutes = new Elysia({
|
||||
prefix: "/community-suggestions",
|
||||
detail: { tags: ["Games"] },
|
||||
})
|
||||
|
||||
// Admin list with pagination and filtering
|
||||
|
||||
+1
-1
@@ -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 }) => {
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ const CATEGORY_LABELS: Record<string, string> = {
|
||||
|
||||
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 {
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ interface CronTaskResult {
|
||||
// ── Task Registry ───────────────────────────────────────────────────
|
||||
type CronTask = () => Promise<CronTaskResult>
|
||||
|
||||
const taskRegistry = new Map<string, CronTask>()
|
||||
export const taskRegistry = new Map<string, CronTask>()
|
||||
|
||||
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
|
||||
|
||||
+26
-1
@@ -60,6 +60,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
|
||||
primaryKey?: string
|
||||
paramName?: string
|
||||
softDelete?: boolean
|
||||
tags?: string[]
|
||||
},
|
||||
) {
|
||||
const {
|
||||
@@ -71,6 +72,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
|
||||
primaryKey = "id",
|
||||
paramName = primaryKey,
|
||||
softDelete = false,
|
||||
tags,
|
||||
} = config
|
||||
|
||||
const columns = getTableColumns(table) as Record<string, PgColumn>
|
||||
@@ -80,7 +82,10 @@ export function createCrudRoutes<T extends AnyPgTable>(
|
||||
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<T extends AnyPgTable>(
|
||||
// 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<T extends AnyPgTable>(
|
||||
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<T extends AnyPgTable>(
|
||||
},
|
||||
{
|
||||
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<T extends AnyPgTable>(
|
||||
[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<T extends AnyPgTable>(
|
||||
params: t.Object({
|
||||
[paramName]: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: `Delete ${name}`,
|
||||
description: `Deletes a ${name} by ID. Requires admin or contributor role.`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
+9
-2
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
+10
-1
@@ -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.",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
+1
-1
@@ -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, [
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
+1
-1
@@ -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 }) => {
|
||||
|
||||
+1
-1
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user