Task 2: Admin Performance Entries API Route
- GET /admin/performance — paginated listing with removed/verified/search filters, joining games, gameVersions, hardware, and user tables - PATCH /admin/performance/:id/verify — verify entry (contributor or admin) - PATCH /admin/performance/:id/remove — soft-delete entry (admin only) - PATCH /admin/performance/:id/restore — restore entry (admin only) - Registered route in app/api/[[...slugs]]/route.ts and lib/api/index.ts
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
reportRoutes,
|
reportRoutes,
|
||||||
contactRoutes,
|
contactRoutes,
|
||||||
adminReportRoutes,
|
adminReportRoutes,
|
||||||
|
adminPerformanceRoutes,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import { steamSearchRoutes } from "@/lib/api/steam-search"
|
import { steamSearchRoutes } from "@/lib/api/steam-search"
|
||||||
import { searchUnifiedRoutes } from "@/lib/api/search-unified"
|
import { searchUnifiedRoutes } from "@/lib/api/search-unified"
|
||||||
@@ -72,6 +73,7 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
.use(performanceSubmitRoutes)
|
.use(performanceSubmitRoutes)
|
||||||
.use(reportRoutes)
|
.use(reportRoutes)
|
||||||
.use(adminReportRoutes)
|
.use(adminReportRoutes)
|
||||||
|
.use(adminPerformanceRoutes)
|
||||||
// Comments
|
// Comments
|
||||||
.use(commentsRoutes)
|
.use(commentsRoutes)
|
||||||
// Steam search proxy
|
// Steam search proxy
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { Elysia, t } from "elysia"
|
||||||
|
import { db } from "@/lib/db/index"
|
||||||
|
import {
|
||||||
|
performanceEntries,
|
||||||
|
gameVersions,
|
||||||
|
games,
|
||||||
|
hardware,
|
||||||
|
user,
|
||||||
|
} from "@/lib/db/schema"
|
||||||
|
import { eq, desc, sql, and, ilike, isNull, isNotNull } from "drizzle-orm"
|
||||||
|
import {
|
||||||
|
requireContributorOrAdmin,
|
||||||
|
requireAdmin,
|
||||||
|
} from "@/lib/auth/guard"
|
||||||
|
|
||||||
|
export const adminPerformanceRoutes = new Elysia({ prefix: "/admin" })
|
||||||
|
.get(
|
||||||
|
"/performance",
|
||||||
|
async ({ query, request, set }) => {
|
||||||
|
const guard = await requireContributorOrAdmin(request.headers)
|
||||||
|
if (!guard.ok) {
|
||||||
|
set.status = guard.status
|
||||||
|
return { error: guard.error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = Math.min(Number(query.limit) || 20, 100)
|
||||||
|
const offset = Number(query.offset) || 0
|
||||||
|
const removedFilter = query.removed
|
||||||
|
const verifiedFilter = query.verified
|
||||||
|
const searchTerm = query.search
|
||||||
|
|
||||||
|
const conditions: (ReturnType<typeof eq> | ReturnType<typeof ilike> | ReturnType<typeof isNull> | ReturnType<typeof isNotNull> | ReturnType<typeof and>)[] = []
|
||||||
|
|
||||||
|
if (removedFilter === "true") {
|
||||||
|
conditions.push(eq(performanceEntries.isRemoved, true))
|
||||||
|
} else if (removedFilter === "false") {
|
||||||
|
conditions.push(eq(performanceEntries.isRemoved, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifiedFilter === "true") {
|
||||||
|
conditions.push(isNotNull(performanceEntries.verifiedAt))
|
||||||
|
} else if (verifiedFilter === "false") {
|
||||||
|
conditions.push(isNull(performanceEntries.verifiedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
conditions.push(ilike(user.name, `%${searchTerm}%`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? and(...conditions) : undefined
|
||||||
|
|
||||||
|
const baseQuery = db
|
||||||
|
.select({
|
||||||
|
id: performanceEntries.id,
|
||||||
|
versionId: performanceEntries.versionId,
|
||||||
|
hardwareSlug: performanceEntries.hardwareSlug,
|
||||||
|
userId: performanceEntries.userId,
|
||||||
|
fpsAvg: performanceEntries.fpsAvg,
|
||||||
|
fpsLow: performanceEntries.fpsLow,
|
||||||
|
fpsHigh: performanceEntries.fpsHigh,
|
||||||
|
protonVersion: performanceEntries.protonVersion,
|
||||||
|
osVersion: performanceEntries.osVersion,
|
||||||
|
upscalerType: performanceEntries.upscalerType,
|
||||||
|
upscalerVersion: performanceEntries.upscalerVersion,
|
||||||
|
frameGenMethod: performanceEntries.frameGenMethod,
|
||||||
|
loadTimeSsd: performanceEntries.loadTimeSsd,
|
||||||
|
loadTimeSd: performanceEntries.loadTimeSd,
|
||||||
|
launchOptions: performanceEntries.launchOptions,
|
||||||
|
settingsJson: performanceEntries.settingsJson,
|
||||||
|
userNotes: performanceEntries.userNotes,
|
||||||
|
estimatedBatteryMin: performanceEntries.estimatedBatteryMin,
|
||||||
|
customSystem: performanceEntries.customSystem,
|
||||||
|
isRemoved: performanceEntries.isRemoved,
|
||||||
|
removedReason: performanceEntries.removedReason,
|
||||||
|
upvotes: performanceEntries.upvotes,
|
||||||
|
downvotes: performanceEntries.downvotes,
|
||||||
|
verifiedAt: performanceEntries.verifiedAt,
|
||||||
|
verifiedBy: performanceEntries.verifiedBy,
|
||||||
|
createdAt: performanceEntries.createdAt,
|
||||||
|
updatedAt: performanceEntries.updatedAt,
|
||||||
|
gameTitle: games.title,
|
||||||
|
versionString: gameVersions.versionString,
|
||||||
|
hardwareName: hardware.name,
|
||||||
|
authorName: user.name,
|
||||||
|
authorImage: user.image,
|
||||||
|
})
|
||||||
|
.from(performanceEntries)
|
||||||
|
.innerJoin(
|
||||||
|
gameVersions,
|
||||||
|
eq(performanceEntries.versionId, gameVersions.id),
|
||||||
|
)
|
||||||
|
.innerJoin(games, eq(gameVersions.gameId, games.id))
|
||||||
|
.innerJoin(
|
||||||
|
hardware,
|
||||||
|
eq(performanceEntries.hardwareSlug, hardware.slug),
|
||||||
|
)
|
||||||
|
.innerJoin(user, eq(performanceEntries.userId, user.id))
|
||||||
|
.orderBy(desc(performanceEntries.createdAt))
|
||||||
|
|
||||||
|
const items = whereClause
|
||||||
|
? await baseQuery.where(whereClause).limit(limit).offset(offset)
|
||||||
|
: await baseQuery.limit(limit).offset(offset)
|
||||||
|
|
||||||
|
const countQuery = db
|
||||||
|
.select({ count: sql<number>`count(*)::int` })
|
||||||
|
.from(performanceEntries)
|
||||||
|
.innerJoin(
|
||||||
|
gameVersions,
|
||||||
|
eq(performanceEntries.versionId, gameVersions.id),
|
||||||
|
)
|
||||||
|
.innerJoin(games, eq(gameVersions.gameId, games.id))
|
||||||
|
.innerJoin(
|
||||||
|
hardware,
|
||||||
|
eq(performanceEntries.hardwareSlug, hardware.slug),
|
||||||
|
)
|
||||||
|
.innerJoin(user, eq(performanceEntries.userId, user.id))
|
||||||
|
|
||||||
|
const countResult = whereClause
|
||||||
|
? await countQuery.where(whereClause)
|
||||||
|
: await countQuery
|
||||||
|
|
||||||
|
const total = countResult[0]?.count ?? 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: items,
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: t.Object({
|
||||||
|
removed: t.Optional(t.Union([t.Literal("true"), t.Literal("false")])),
|
||||||
|
verified: t.Optional(t.Union([t.Literal("true"), t.Literal("false")])),
|
||||||
|
search: t.Optional(t.String()),
|
||||||
|
limit: t.Optional(t.String()),
|
||||||
|
offset: t.Optional(t.String()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.patch(
|
||||||
|
"/performance/:id/verify",
|
||||||
|
async ({ params, request, set }) => {
|
||||||
|
const guard = await requireContributorOrAdmin(request.headers)
|
||||||
|
if (!guard.ok) {
|
||||||
|
set.status = guard.status
|
||||||
|
return { error: guard.error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [entry] = await db
|
||||||
|
.select()
|
||||||
|
.from(performanceEntries)
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
set.status = 404
|
||||||
|
return { error: "Performance entry not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.verifiedAt) {
|
||||||
|
set.status = 409
|
||||||
|
return { error: "Entry already verified" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(performanceEntries)
|
||||||
|
.set({
|
||||||
|
verifiedAt: new Date(),
|
||||||
|
verifiedBy: guard.user.id,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return updated
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({ id: t.String() }),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.patch(
|
||||||
|
"/performance/:id/remove",
|
||||||
|
async ({ params, body, request, set }) => {
|
||||||
|
const guard = await requireAdmin(request.headers)
|
||||||
|
if (!guard.ok) {
|
||||||
|
set.status = guard.status
|
||||||
|
return { error: guard.error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [entry] = await db
|
||||||
|
.select()
|
||||||
|
.from(performanceEntries)
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
set.status = 404
|
||||||
|
return { error: "Performance entry not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(performanceEntries)
|
||||||
|
.set({
|
||||||
|
isRemoved: true,
|
||||||
|
removedReason: body?.reason ?? "Admin removed",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return updated
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({ id: t.String() }),
|
||||||
|
body: t.Optional(
|
||||||
|
t.Object({
|
||||||
|
reason: t.Optional(t.String()),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.patch(
|
||||||
|
"/performance/:id/restore",
|
||||||
|
async ({ params, request, set }) => {
|
||||||
|
const guard = await requireAdmin(request.headers)
|
||||||
|
if (!guard.ok) {
|
||||||
|
set.status = guard.status
|
||||||
|
return { error: guard.error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [entry] = await db
|
||||||
|
.select()
|
||||||
|
.from(performanceEntries)
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
set.status = 404
|
||||||
|
return { error: "Performance entry not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(performanceEntries)
|
||||||
|
.set({
|
||||||
|
isRemoved: false,
|
||||||
|
removedReason: null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return updated
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({ id: t.String() }),
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -12,3 +12,4 @@ export { savedGamesRoutes } from "./saved-games"
|
|||||||
export { reportRoutes } from "./reports"
|
export { reportRoutes } from "./reports"
|
||||||
export { contactRoutes } from "./contact"
|
export { contactRoutes } from "./contact"
|
||||||
export { adminReportRoutes } from "./admin-reports"
|
export { adminReportRoutes } from "./admin-reports"
|
||||||
|
export { adminPerformanceRoutes } from "./admin-performance"
|
||||||
|
|||||||
Reference in New Issue
Block a user