릴리스: v0.1.13 관리자 탭과 가변 티어 행 추가
This commit is contained in:
@@ -296,6 +296,11 @@ async function adminUpdateUser({ id, email, nickname, isAdmin }) {
|
||||
return findUserById(id)
|
||||
}
|
||||
|
||||
async function adminUpdateUserPassword({ id, passwordHash }) {
|
||||
await query('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, id])
|
||||
return findUserById(id)
|
||||
}
|
||||
|
||||
async function adminDeleteUser(id) {
|
||||
await query('DELETE FROM users WHERE id = ?', [id])
|
||||
}
|
||||
@@ -399,7 +404,25 @@ async function createCustomItem({ id, ownerId, src, label }) {
|
||||
return { id, ownerId, src, label, origin: 'custom', createdAt }
|
||||
}
|
||||
|
||||
async function listCustomItems() {
|
||||
async function listCustomItems({ queryText = '', page = 1, limit = 50 } = {}) {
|
||||
const normalizedLimit = Math.min(Math.max(Number(limit) || 50, 1), 200)
|
||||
const normalizedPage = Math.max(Number(page) || 1, 1)
|
||||
const offset = (normalizedPage - 1) * normalizedLimit
|
||||
const hasQuery = !!(queryText || '').trim()
|
||||
const search = `%${(queryText || '').trim()}%`
|
||||
const whereClause = hasQuery ? 'WHERE c.label LIKE ? OR c.src LIKE ? OR u.email LIKE ? OR u.nickname LIKE ?' : ''
|
||||
const params = hasQuery ? [search, search, search, search] : []
|
||||
|
||||
const countRows = await query(
|
||||
`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM custom_items c
|
||||
INNER JOIN users u ON u.id = c.owner_id
|
||||
${whereClause}
|
||||
`,
|
||||
params
|
||||
)
|
||||
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
@@ -412,20 +435,27 @@ async function listCustomItems() {
|
||||
u.email
|
||||
FROM custom_items c
|
||||
INNER JOIN users u ON u.id = c.owner_id
|
||||
${whereClause}
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT 200
|
||||
`
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
[...params, normalizedLimit, offset]
|
||||
)
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
ownerId: row.owner_id,
|
||||
src: row.src,
|
||||
label: row.label,
|
||||
createdAt: Number(row.created_at),
|
||||
ownerName: row.nickname || row.email,
|
||||
ownerEmail: row.email,
|
||||
}))
|
||||
return {
|
||||
items: rows.map((row) => ({
|
||||
id: row.id,
|
||||
ownerId: row.owner_id,
|
||||
src: row.src,
|
||||
label: row.label,
|
||||
createdAt: Number(row.created_at),
|
||||
ownerName: row.nickname || row.email,
|
||||
ownerEmail: row.email,
|
||||
})),
|
||||
total: Number(countRows[0]?.count || 0),
|
||||
page: normalizedPage,
|
||||
limit: normalizedLimit,
|
||||
}
|
||||
}
|
||||
|
||||
async function listPublicTierLists(gameId) {
|
||||
@@ -539,6 +569,7 @@ module.exports = {
|
||||
updateUserProfile,
|
||||
listUsers,
|
||||
adminUpdateUser,
|
||||
adminUpdateUserPassword,
|
||||
adminDeleteUser,
|
||||
listGames,
|
||||
findGameById,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const path = require('path')
|
||||
const express = require('express')
|
||||
const multer = require('multer')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const { z } = require('zod')
|
||||
const { nanoid } = require('nanoid')
|
||||
const {
|
||||
@@ -14,6 +15,7 @@ const {
|
||||
listCustomItems,
|
||||
listUsers,
|
||||
adminUpdateUser,
|
||||
adminUpdateUserPassword,
|
||||
adminDeleteUser,
|
||||
} = require('../db')
|
||||
const { requireAdmin } = require('../middleware/auth')
|
||||
@@ -83,8 +85,20 @@ router.delete('/games/:gameId', requireAdmin, async (req, res) => {
|
||||
})
|
||||
|
||||
router.get('/custom-items', requireAdmin, async (req, res) => {
|
||||
const items = await listCustomItems()
|
||||
res.json({ items })
|
||||
const schema = z.object({
|
||||
q: z.string().trim().max(120).optional().default(''),
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional().default(50),
|
||||
})
|
||||
const parsed = schema.safeParse(req.query)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const result = await listCustomItems({
|
||||
queryText: parsed.data.q,
|
||||
page: parsed.data.page,
|
||||
limit: parsed.data.limit,
|
||||
})
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.get('/users', requireAdmin, async (req, res) => {
|
||||
@@ -136,4 +150,19 @@ router.delete('/users/:userId', requireAdmin, async (req, res) => {
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
router.patch('/users/:userId/password', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
password: z.string().min(6).max(120),
|
||||
})
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const user = await findUserById(req.params.userId)
|
||||
if (!user) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const passwordHash = await bcrypt.hash(parsed.data.password, 10)
|
||||
await adminUpdateUserPassword({ id: user.id, passwordHash })
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
Reference in New Issue
Block a user