- Add plugin_pairings table + migration (0028) for short-lived pairing sessions - Add pairing API: POST /api/plugin/pair/initiate, GET /status/:token, POST /confirm - Add /pair web page: user scans QR on phone, confirms, API key auto-created - Plugin: 'Pair with Phone' button renders QR code (qrcode.react), polls for status, saves API key - Plugin: new initiate_pair + check_pair_status Python RPCs - Revise MangoHud Setup Guide into clean numbered steps - Fix SteamOS version detection: read VERSION_ID + BUILD_ID (was just 'SteamOS')
282 lines
9.7 KiB
TypeScript
282 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"
|
|
|
|
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)
|
|
)
|
|
// ── 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
|