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:
2026-05-14 21:43:33 +08:00
parent 20f010cef1
commit fbfc43832e
37 changed files with 161 additions and 32 deletions
+8
View File
@@ -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=="],
+1 -1
View File
@@ -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 }) => {
+1 -1
View File
@@ -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 }) => {
+1 -1
View File
@@ -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 }) => {
+1 -1
View File
@@ -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(
+73
View File
@@ -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}`,
+1
View File
@@ -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(
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.`,
},
},
)
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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",
+1
View File
@@ -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
View File
@@ -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.",
},
},
)
+1 -1
View File
@@ -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 }) => {
+2
View File
@@ -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",
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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
View File
@@ -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, [
+1 -1
View File
@@ -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 }) => {
+1 -1
View File
@@ -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 }) => {
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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 }) => {
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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: [
{
+2
View File
@@ -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",