fix(api): fix Response.clone body consumed error on /api/openapi
Add embedSpec: true to openapi config to prevent cached Response body from being consumed on subsequent requests. Also add games-performance routes export and mobile-callback route.
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
import { auth } from "@/lib/auth"
|
||||||
|
import { NextRequest } from "next/server"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mobile OAuth bridge page.
|
||||||
|
*
|
||||||
|
* After a social OAuth flow completes in the mobile WebBrowser,
|
||||||
|
* Better Auth redirects here with the session cookie set.
|
||||||
|
* We extract the session token and redirect to the app's
|
||||||
|
* deep link scheme so the mobile app can store the token.
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
// No session found — redirect to app with error
|
||||||
|
const errorUrl = new URL("deckyvault://auth/callback")
|
||||||
|
errorUrl.searchParams.set("error", "session_not_found")
|
||||||
|
return Response.redirect(errorUrl.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = session.session.token
|
||||||
|
const callbackUrl = new URL("deckyvault://auth/callback")
|
||||||
|
callbackUrl.searchParams.set("token", token)
|
||||||
|
|
||||||
|
return Response.redirect(callbackUrl.toString())
|
||||||
|
} catch {
|
||||||
|
const errorUrl = new URL("deckyvault://auth/callback")
|
||||||
|
errorUrl.searchParams.set("error", "callback_error")
|
||||||
|
return Response.redirect(errorUrl.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { steamdbVersionRoutes } from "@/lib/api/steamdb-version"
|
|||||||
import { searchUnifiedRoutes } from "@/lib/api/search-unified"
|
import { searchUnifiedRoutes } from "@/lib/api/search-unified"
|
||||||
import { gameStubRoutes } from "@/lib/api/game-stub"
|
import { gameStubRoutes } from "@/lib/api/game-stub"
|
||||||
import { gameStatsRoutes } from "@/lib/api/game-stats"
|
import { gameStatsRoutes } from "@/lib/api/game-stats"
|
||||||
|
import { gamesPerformanceRoutes } from "@/lib/api/games-performance"
|
||||||
import { gamesManualRoutes } from "@/lib/api/games-manual"
|
import { gamesManualRoutes } from "@/lib/api/games-manual"
|
||||||
import { compareRoutes } from "@/lib/api/compare"
|
import { compareRoutes } from "@/lib/api/compare"
|
||||||
import { playabilityRoutes } from "@/lib/api/playability"
|
import { playabilityRoutes } from "@/lib/api/playability"
|
||||||
@@ -66,6 +67,7 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
.use(
|
.use(
|
||||||
openapi({
|
openapi({
|
||||||
path: "/openapi",
|
path: "/openapi",
|
||||||
|
embedSpec: true,
|
||||||
documentation: {
|
documentation: {
|
||||||
info: {
|
info: {
|
||||||
title: "DeckyVault API",
|
title: "DeckyVault API",
|
||||||
@@ -164,6 +166,7 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
.use(hardwareStatsRoutes)
|
.use(hardwareStatsRoutes)
|
||||||
.use(performanceRoutes)
|
.use(performanceRoutes)
|
||||||
.use(gameStatsRoutes)
|
.use(gameStatsRoutes)
|
||||||
|
.use(gamesPerformanceRoutes)
|
||||||
.use(dashboardRoutes)
|
.use(dashboardRoutes)
|
||||||
.use(dashboardPublicRoutes)
|
.use(dashboardPublicRoutes)
|
||||||
.use(playabilityRoutes)
|
.use(playabilityRoutes)
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { Elysia, t } from "elysia"
|
||||||
|
import { db } from "@/lib/db/index"
|
||||||
|
import {
|
||||||
|
gameVersions,
|
||||||
|
performanceEntries,
|
||||||
|
hardware,
|
||||||
|
user,
|
||||||
|
gamePlatformSupport,
|
||||||
|
entryScreenshots,
|
||||||
|
} from "@/lib/db/schema"
|
||||||
|
import { and, desc, eq, sql } from "drizzle-orm"
|
||||||
|
import { getR2PublicUrl } from "@/lib/storage"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/games/:gameId/performance
|
||||||
|
*
|
||||||
|
* Returns all non-removed performance entries (presets) for a game,
|
||||||
|
* with joined user, hardware, platform support, and screenshot data.
|
||||||
|
* Ordered by isPinned desc, then upvotes desc.
|
||||||
|
*/
|
||||||
|
export const gamesPerformanceRoutes = new Elysia({
|
||||||
|
prefix: "/games",
|
||||||
|
detail: { tags: ["Games"] },
|
||||||
|
}).get(
|
||||||
|
"/:gameId/performance",
|
||||||
|
async ({ params, set }) => {
|
||||||
|
const { gameId } = params
|
||||||
|
|
||||||
|
const presetRows = await db
|
||||||
|
.select({
|
||||||
|
id: performanceEntries.id,
|
||||||
|
hardwareSlug: performanceEntries.hardwareSlug,
|
||||||
|
hardwareName: hardware.name,
|
||||||
|
upvotes: performanceEntries.upvotes,
|
||||||
|
settingsJson: performanceEntries.settingsJson,
|
||||||
|
fpsAvg: performanceEntries.fpsAvg,
|
||||||
|
fpsLow: performanceEntries.fpsLow,
|
||||||
|
fpsHigh: performanceEntries.fpsHigh,
|
||||||
|
fpsOnePercentLow: performanceEntries.fpsOnePercentLow,
|
||||||
|
upscalerType: performanceEntries.upscalerType,
|
||||||
|
upscalerVersion: performanceEntries.upscalerVersion,
|
||||||
|
frameGenMethod: performanceEntries.frameGenMethod,
|
||||||
|
protonVersion: performanceEntries.protonVersion,
|
||||||
|
osVersion: performanceEntries.osVersion,
|
||||||
|
createdAt: performanceEntries.createdAt,
|
||||||
|
userId: performanceEntries.userId,
|
||||||
|
userName: user.name,
|
||||||
|
userImage: user.image,
|
||||||
|
downvotes: performanceEntries.downvotes,
|
||||||
|
launchOptions: performanceEntries.launchOptions,
|
||||||
|
loadTimeSsd: performanceEntries.loadTimeSsd,
|
||||||
|
loadTimeSd: performanceEntries.loadTimeSd,
|
||||||
|
tdpWatts: performanceEntries.tdpWatts,
|
||||||
|
youtubeVideoId: performanceEntries.youtubeVideoId,
|
||||||
|
customSystem: performanceEntries.customSystem,
|
||||||
|
userNotes: performanceEntries.userNotes,
|
||||||
|
versionString: gameVersions.versionString,
|
||||||
|
buildId: gameVersions.buildId,
|
||||||
|
gameAntiCheatName: gamePlatformSupport.antiCheatName,
|
||||||
|
gameAntiCheatStatus: gamePlatformSupport.antiCheatStatus,
|
||||||
|
verifiedAt: performanceEntries.verifiedAt,
|
||||||
|
isPinned: performanceEntries.isPinned,
|
||||||
|
pinnedAt: performanceEntries.pinnedAt,
|
||||||
|
})
|
||||||
|
.from(performanceEntries)
|
||||||
|
.innerJoin(
|
||||||
|
gameVersions,
|
||||||
|
eq(performanceEntries.versionId, gameVersions.id),
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
hardware,
|
||||||
|
eq(performanceEntries.hardwareSlug, hardware.slug),
|
||||||
|
)
|
||||||
|
.innerJoin(user, eq(performanceEntries.userId, user.id))
|
||||||
|
.innerJoin(
|
||||||
|
gamePlatformSupport,
|
||||||
|
and(
|
||||||
|
eq(gamePlatformSupport.gameId, gameVersions.gameId),
|
||||||
|
eq(
|
||||||
|
gamePlatformSupport.hardwareSlug,
|
||||||
|
performanceEntries.hardwareSlug,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(gameVersions.gameId, gameId),
|
||||||
|
eq(performanceEntries.isRemoved, false),
|
||||||
|
sql`${performanceEntries.settingsJson} IS NOT NULL`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(
|
||||||
|
desc(performanceEntries.isPinned),
|
||||||
|
desc(performanceEntries.upvotes),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (presetRows.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicUrl = getR2PublicUrl()
|
||||||
|
|
||||||
|
// Fetch screenshots and hardware details per preset
|
||||||
|
const presets = await Promise.all(
|
||||||
|
presetRows.map(async (p) => {
|
||||||
|
const screenshots = await db
|
||||||
|
.select({
|
||||||
|
id: entryScreenshots.id,
|
||||||
|
storageKey: entryScreenshots.storageKey,
|
||||||
|
orderIndex: entryScreenshots.orderIndex,
|
||||||
|
width: entryScreenshots.width,
|
||||||
|
height: entryScreenshots.height,
|
||||||
|
})
|
||||||
|
.from(entryScreenshots)
|
||||||
|
.where(eq(entryScreenshots.entryId, p.id))
|
||||||
|
.orderBy(entryScreenshots.orderIndex)
|
||||||
|
|
||||||
|
const [hw] = await db
|
||||||
|
.select({
|
||||||
|
wattHours: hardware.wattHours,
|
||||||
|
deviceType: hardware.deviceType,
|
||||||
|
})
|
||||||
|
.from(hardware)
|
||||||
|
.where(eq(hardware.slug, p.hardwareSlug))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
const settingsCount = Array.isArray(p.settingsJson)
|
||||||
|
? (p.settingsJson as Array<{ settings: unknown[] }>).reduce(
|
||||||
|
(sum, cat) => sum + cat.settings.length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: p.id,
|
||||||
|
gameId,
|
||||||
|
hardwareSlug: p.hardwareSlug,
|
||||||
|
hardwareName: p.hardwareName,
|
||||||
|
upvotes: p.upvotes,
|
||||||
|
downvotes: p.downvotes,
|
||||||
|
settingsCount,
|
||||||
|
fpsAvg: p.fpsAvg,
|
||||||
|
fpsLow: p.fpsLow,
|
||||||
|
fpsHigh: p.fpsHigh,
|
||||||
|
fpsOnePercentLow: p.fpsOnePercentLow ?? null,
|
||||||
|
upscalerType: p.upscalerType,
|
||||||
|
upscalerVersion: p.upscalerVersion,
|
||||||
|
frameGenMethod: p.frameGenMethod,
|
||||||
|
protonVersion: p.protonVersion,
|
||||||
|
osVersion: p.osVersion,
|
||||||
|
createdAt: p.createdAt.toISOString(),
|
||||||
|
settingsJson: p.settingsJson,
|
||||||
|
launchOptions: p.launchOptions,
|
||||||
|
loadTimeSsd: p.loadTimeSsd ?? null,
|
||||||
|
loadTimeSd: p.loadTimeSd ?? null,
|
||||||
|
tdpWatts: p.tdpWatts ?? null,
|
||||||
|
youtubeVideoId: p.youtubeVideoId ?? null,
|
||||||
|
screenshots: screenshots.map((ss) => ({
|
||||||
|
id: ss.id,
|
||||||
|
url: `${publicUrl}/${ss.storageKey}`,
|
||||||
|
width: ss.width,
|
||||||
|
height: ss.height,
|
||||||
|
orderIndex: ss.orderIndex,
|
||||||
|
})),
|
||||||
|
hardwareWattHours: hw?.wattHours ? Number(hw.wattHours) : null,
|
||||||
|
hardwareDeviceType: hw?.deviceType ?? null,
|
||||||
|
customSystem: p.customSystem ?? false,
|
||||||
|
userNotes: p.userNotes,
|
||||||
|
versionString: p.versionString ?? null,
|
||||||
|
buildId: p.buildId ?? null,
|
||||||
|
gameAntiCheatName: p.gameAntiCheatName ?? null,
|
||||||
|
gameAntiCheatStatus: p.gameAntiCheatStatus ?? null,
|
||||||
|
userId: p.userId,
|
||||||
|
userName: p.userName,
|
||||||
|
userImage: p.userImage,
|
||||||
|
verifiedAt: p.verifiedAt ? p.verifiedAt.toISOString() : null,
|
||||||
|
isPinned: p.isPinned,
|
||||||
|
pinnedAt: p.pinnedAt ? p.pinnedAt.toISOString() : null,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return presets
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({ gameId: t.String() }),
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -7,6 +7,7 @@ export { performanceSubmitRoutes } from "./performance-submit"
|
|||||||
export { commentsRoutes } from "./comments"
|
export { commentsRoutes } from "./comments"
|
||||||
export { gamesListingRoutes } from "./games-listing"
|
export { gamesListingRoutes } from "./games-listing"
|
||||||
export { gameStatsRoutes } from "./game-stats"
|
export { gameStatsRoutes } from "./game-stats"
|
||||||
|
export { gamesPerformanceRoutes } from "./games-performance"
|
||||||
export { hardwareStatsRoutes } from "./hardware-stats"
|
export { hardwareStatsRoutes } from "./hardware-stats"
|
||||||
export { savedGamesRoutes } from "./saved-games"
|
export { savedGamesRoutes } from "./saved-games"
|
||||||
export { reportRoutes } from "./reports"
|
export { reportRoutes } from "./reports"
|
||||||
|
|||||||
Reference in New Issue
Block a user