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