feat: add admin comments API route

- GET /admin/comments: list all comments with pagination, removed filter, and author search
- PATCH /admin/comments/:id/remove: admin-only soft delete
- PATCH /admin/comments/:id/restore: admin-only restore
This commit is contained in:
2026-04-28 07:55:57 +08:00
parent 6d27ec03ab
commit 2ab4ac5ebc
3 changed files with 167 additions and 0 deletions
+2
View File
@@ -17,6 +17,7 @@ import {
contactRoutes,
adminReportRoutes,
adminPerformanceRoutes,
adminCommentRoutes,
} from "@/lib/api"
import { steamSearchRoutes } from "@/lib/api/steam-search"
import { searchUnifiedRoutes } from "@/lib/api/search-unified"
@@ -74,6 +75,7 @@ export const app = new Elysia({ prefix: "/api" })
.use(reportRoutes)
.use(adminReportRoutes)
.use(adminPerformanceRoutes)
.use(adminCommentRoutes)
// Comments
.use(commentsRoutes)
// Steam search proxy
+164
View File
@@ -0,0 +1,164 @@
import { Elysia, t } from "elysia"
import { db } from "@/lib/db/index"
import {
gameComments,
games,
user,
} from "@/lib/db/schema"
import { eq, desc, sql, and, ilike } from "drizzle-orm"
import {
requireContributorOrAdmin,
requireAdmin,
} from "@/lib/auth/guard"
export const adminCommentRoutes = new Elysia({ prefix: "/admin" })
.get(
"/comments",
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 searchTerm = query.search
const conditions: (ReturnType<typeof eq> | ReturnType<typeof ilike> | ReturnType<typeof and>)[] = []
if (removedFilter === "true") {
conditions.push(eq(gameComments.isRemoved, true))
} else if (removedFilter === "false") {
conditions.push(eq(gameComments.isRemoved, false))
}
if (searchTerm) {
conditions.push(ilike(user.name, `%${searchTerm}%`))
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined
const baseQuery = db
.select({
id: gameComments.id,
gameId: gameComments.gameId,
content: gameComments.content,
upvotes: gameComments.upvotes,
isRemoved: gameComments.isRemoved,
createdAt: gameComments.createdAt,
updatedAt: gameComments.updatedAt,
userId: gameComments.userId,
userName: user.name,
userImage: user.image,
gameTitle: games.title,
parentId: gameComments.parentId,
})
.from(gameComments)
.innerJoin(games, eq(gameComments.gameId, games.id))
.innerJoin(user, eq(gameComments.userId, user.id))
.orderBy(desc(gameComments.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(gameComments)
.innerJoin(games, eq(gameComments.gameId, games.id))
.innerJoin(user, eq(gameComments.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")])),
search: t.Optional(t.String()),
limit: t.Optional(t.String()),
offset: t.Optional(t.String()),
}),
},
)
.patch(
"/comments/:id/remove",
async ({ params, request, set }) => {
const guard = await requireAdmin(request.headers)
if (!guard.ok) {
set.status = guard.status
return { error: guard.error }
}
const [comment] = await db
.select()
.from(gameComments)
.where(eq(gameComments.id, params.id))
.limit(1)
if (!comment) {
set.status = 404
return { error: "Comment not found" }
}
const [updated] = await db
.update(gameComments)
.set({
isRemoved: true,
updatedAt: new Date(),
})
.where(eq(gameComments.id, params.id))
.returning()
return updated
},
{
params: t.Object({ id: t.String() }),
},
)
.patch(
"/comments/:id/restore",
async ({ params, request, set }) => {
const guard = await requireAdmin(request.headers)
if (!guard.ok) {
set.status = guard.status
return { error: guard.error }
}
const [comment] = await db
.select()
.from(gameComments)
.where(eq(gameComments.id, params.id))
.limit(1)
if (!comment) {
set.status = 404
return { error: "Comment not found" }
}
const [updated] = await db
.update(gameComments)
.set({
isRemoved: false,
updatedAt: new Date(),
})
.where(eq(gameComments.id, params.id))
.returning()
return updated
},
{
params: t.Object({ id: t.String() }),
},
)
+1
View File
@@ -13,3 +13,4 @@ export { reportRoutes } from "./reports"
export { contactRoutes } from "./contact"
export { adminReportRoutes } from "./admin-reports"
export { adminPerformanceRoutes } from "./admin-performance"
export { adminCommentRoutes } from "./admin-comments"