릴리스: v0.1.13 관리자 탭과 가변 티어 행 추가

This commit is contained in:
2026-03-19 16:58:17 +09:00
parent f6a031cfe4
commit b97d7eacda
10 changed files with 484 additions and 215 deletions

View File

@@ -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,

View File

@@ -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