Files
deckyvault/apps/web/lib/api/app.ts
T

284 lines
9.7 KiB
TypeScript

import { Elysia, t } from "elysia"
import { openapi } from "@elysia/openapi"
import { cron, Patterns } from "@elysia/cron"
import { auth } from "@/lib/auth"
import { isDeckyVaultEmail, DOMAIN_BLOCK_ERROR } from "@/lib/auth/domain-block"
import { taskRegistry } from "./cron"
import {
healthRoutes,
userRoutes,
gamesRoutes,
gameVersionsRoutes,
gameSyncRoutes,
hardwareRoutes,
hardwareStatsRoutes,
performanceRoutes,
performanceVerifyRoutes,
performanceSubmitRoutes,
commentsRoutes,
savedGamesRoutes,
reportRoutes,
contactRoutes,
adminReportRoutes,
adminPerformanceRoutes,
adminCommentRoutes,
} from "@/lib/api"
import { adminStorageRoutes } from "@/lib/api/admin-storage"
import { adminAnalyticsRoutes } from "@/lib/api/admin-analytics"
import { steamSearchRoutes } from "@/lib/api/steam-search"
import { steamdbVersionRoutes, clientVersionRoutes } from "@/lib/api/steamdb-version"
import { versionTestRoutes, standaloneVersionTestRoutes } from "@/lib/api/version-test"
import { searchUnifiedRoutes } from "@/lib/api/search-unified"
import { gameStubRoutes } from "@/lib/api/game-stub"
import { gameStatsRoutes } from "@/lib/api/game-stats"
import { gamesPerformanceRoutes } from "@/lib/api/games-performance"
import { gamesManualRoutes } from "@/lib/api/games-manual"
import { compareRoutes } from "@/lib/api/compare"
import { playabilityRoutes } from "@/lib/api/playability"
import { steamReviewRoutes } from "@/lib/api/steam-reviews"
import { communitySuggestionRoutes } from "@/lib/api/community-suggestions"
import { gamesListingRoutes } from "@/lib/api/games-listing"
import { steamgridProxyRoutes } from "@/lib/api/steamgrid-proxy"
import { dashboardRoutes } from "@/lib/api/dashboard"
import { dashboardPublicRoutes } from "@/lib/api/dashboard-public"
import { savedFilterRoutes } from "@/lib/api/saved-filters"
import { cronRoutes } from "@/lib/api/cron"
import { profilePhotoRoutes } from "@/lib/api/profile-photo"
import { screenshotRoutes } from "@/lib/api/screenshots"
import { loginEmailSchema } from "@/lib/auth/validation"
import { db } from "@/lib/db"
import { user } from "@/lib/db/schema/auth"
import { eq } from "drizzle-orm"
import { mobileRoutes } from "@/lib/api/mobile"
import { gamesLookupRoutes } from "@/lib/api/games-lookup"
import { performanceImportRoutes } from "@/lib/api/performance-import"
import { pluginPairingRoutes } from "@/lib/api/plugin-pairing"
import { pluginPublicRoutes } from "@/lib/api/plugin-public"
const betterAuth = new Elysia({ name: "better-auth" })
.mount(auth.handler)
.macro({
auth: {
async resolve({ status, request: { headers } }) {
const session = await auth.api.getSession({
headers,
})
if (!session) return status(401)
return {
user: session.user,
session: session.session,
}
},
},
})
export const app = new Elysia({ prefix: "/api" })
.use(
openapi({
path: "/openapi",
embedSpec: true,
documentation: {
info: {
title: "DeckyVault API",
version: "2026.2.2",
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: "Mobile", description: "Mobile-optimized consolidated 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}`,
error instanceof Error ? error.message : error,
)
set.status = code === "NOT_FOUND" ? 404 : 500
return {
error: code === "NOT_FOUND" ? "Not found" : "Internal server error",
}
})
// ── Auth routes ─────────────────────────────────────────────
.group("", (app) =>
app
.onBeforeHandle(async ({ request, set }) => {
const url = new URL(request.url)
const isSignUp =
url.pathname === "/api/auth/sign-up/email" &&
request.method === "POST"
if (!isSignUp) return
if (process.env.NODE_ENV !== "development") {
try {
const cloned = request.clone()
const body = await cloned.json()
if (isDeckyVaultEmail(body.email)) {
console.warn(
"[AUTH] Blocked sign-up attempt with deckyvault.xyz email",
body.email,
)
set.status = 400
return { error: DOMAIN_BLOCK_ERROR }
}
} catch {
// Malformed body — let Better Auth reject downstream
}
}
})
.use(betterAuth)
.use(userRoutes)
.use(profilePhotoRoutes)
.post(
"/auth/check-email",
async ({ body, set }) => {
const result = loginEmailSchema.safeParse(body)
if (!result.success) {
set.status = 400
return { error: result.error.issues[0].message }
}
const { email } = result.data
const existingUser = await db
.select({ id: user.id })
.from(user)
.where(eq(user.email, email.toLowerCase()))
.limit(1)
return { exists: existingUser.length > 0 }
},
{
body: t.Object({ email: t.String() }),
response: t.Union([
t.Object({ exists: t.Boolean() }),
t.Object({ error: t.String() }),
]),
detail: { hide: true },
},
)
)
// ── Read-heavy public routes ──────────────────────────────
.group("", (app) =>
app
.use(healthRoutes)
.use(gamesRoutes)
.use(gameVersionsRoutes)
.use(gameSyncRoutes)
.use(gamesListingRoutes)
.use(hardwareRoutes)
.use(hardwareStatsRoutes)
.use(performanceRoutes)
.use(gameStatsRoutes)
.use(gamesPerformanceRoutes)
.use(dashboardRoutes)
.use(dashboardPublicRoutes)
.use(playabilityRoutes)
.use(steamReviewRoutes)
.use(compareRoutes)
.use(savedGamesRoutes)
.use(savedFilterRoutes)
.use(steamSearchRoutes)
.use(searchUnifiedRoutes)
.use(gameStubRoutes)
.use(steamgridProxyRoutes)
.use(steamdbVersionRoutes)
.use(versionTestRoutes)
.use(standaloneVersionTestRoutes)
.use(gamesManualRoutes)
.use(screenshotRoutes)
.use(gamesLookupRoutes)
.use(mobileRoutes)
.use(pluginPairingRoutes)
.use(pluginPublicRoutes)
)
// ── Write routes ───────────────────────────────────────────
.group("", (app) =>
app
.use(betterAuth)
.use(clientVersionRoutes)
.use(performanceVerifyRoutes)
.use(performanceSubmitRoutes)
.use(commentsRoutes)
.use(reportRoutes)
.use(adminReportRoutes)
.use(adminPerformanceRoutes)
.use(adminCommentRoutes)
.use(adminStorageRoutes)
.use(performanceImportRoutes)
.use(adminAnalyticsRoutes)
)
// ── Public forms (no auth) ─────────────────────────────────
.group("", (app) =>
app
.use(contactRoutes)
.use(communitySuggestionRoutes)
)
// ── Cron ─────────────────────────────────────────────────────
.use(cronRoutes)
// ── Root ────────────────────────────────────────────────────
.get("/", () => ({
name: "DeckyVault API",
version: "2026.2.2",
}))
export type App = typeof app