릴리스: v1.2.26 관리자 회원 관리와 셸 UI 개선
This commit is contained in:
@@ -78,6 +78,7 @@ function mapTierListRow(row) {
|
||||
thumbnailSrc: row.thumbnail_src || '',
|
||||
description: row.description || '',
|
||||
isPublic: !!row.is_public,
|
||||
showCharacterNames: !!row.show_character_names,
|
||||
groups: parseJson(row.groups_json, []),
|
||||
pool: parseJson(row.pool_json, []),
|
||||
createdAt: Number(row.created_at),
|
||||
@@ -226,6 +227,7 @@ async function ensureSchema() {
|
||||
thumbnail_src VARCHAR(255) NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL,
|
||||
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
||||
show_character_names TINYINT(1) NOT NULL DEFAULT 0,
|
||||
groups_json LONGTEXT NOT NULL,
|
||||
pool_json LONGTEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
@@ -277,6 +279,10 @@ async function ensureSchema() {
|
||||
if (!tierListThumbnailColumns.length) {
|
||||
await query("ALTER TABLE tierlists ADD COLUMN thumbnail_src VARCHAR(255) NOT NULL DEFAULT '' AFTER title")
|
||||
}
|
||||
const tierListShowNamesColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'show_character_names'")
|
||||
if (!tierListShowNamesColumns.length) {
|
||||
await query("ALTER TABLE tierlists ADD COLUMN show_character_names TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public")
|
||||
}
|
||||
|
||||
await query(
|
||||
`
|
||||
@@ -396,13 +402,23 @@ async function listUsers() {
|
||||
return rows.map(mapUserRow)
|
||||
}
|
||||
|
||||
async function adminUpdateUser({ id, email, nickname, isAdmin }) {
|
||||
await query('UPDATE users SET email = ?, nickname = ?, is_admin = ? WHERE id = ?', [
|
||||
email,
|
||||
nickname || '',
|
||||
isAdmin ? 1 : 0,
|
||||
id,
|
||||
])
|
||||
async function adminUpdateUser({ id, email, nickname, isAdmin, avatarSrc }) {
|
||||
if (typeof avatarSrc === 'string') {
|
||||
await query('UPDATE users SET email = ?, nickname = ?, is_admin = ?, avatar_src = ? WHERE id = ?', [
|
||||
email,
|
||||
nickname || '',
|
||||
isAdmin ? 1 : 0,
|
||||
avatarSrc,
|
||||
id,
|
||||
])
|
||||
} else {
|
||||
await query('UPDATE users SET email = ?, nickname = ?, is_admin = ? WHERE id = ?', [
|
||||
email,
|
||||
nickname || '',
|
||||
isAdmin ? 1 : 0,
|
||||
id,
|
||||
])
|
||||
}
|
||||
return findUserById(id)
|
||||
}
|
||||
|
||||
@@ -833,6 +849,7 @@ async function listFavoriteTierLists(userId, { queryText = '', sort = 'favorited
|
||||
t.thumbnail_src,
|
||||
t.description,
|
||||
t.is_public,
|
||||
t.show_character_names,
|
||||
t.groups_json,
|
||||
t.pool_json,
|
||||
t.created_at,
|
||||
@@ -962,6 +979,7 @@ async function listAdminTierLists({ queryText = '', page = 1, limit = 50, curren
|
||||
t.thumbnail_src,
|
||||
t.description,
|
||||
t.is_public,
|
||||
t.show_character_names,
|
||||
t.groups_json,
|
||||
t.pool_json,
|
||||
t.created_at,
|
||||
@@ -1017,6 +1035,7 @@ async function findTierListById(id, currentUserId = '') {
|
||||
t.thumbnail_src,
|
||||
t.description,
|
||||
t.is_public,
|
||||
t.show_character_names,
|
||||
t.groups_json,
|
||||
t.pool_json,
|
||||
t.created_at,
|
||||
@@ -1214,7 +1233,7 @@ async function deleteCustomItems(ids) {
|
||||
await query(`DELETE FROM custom_items WHERE id IN (${placeholders})`, ids)
|
||||
}
|
||||
|
||||
async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', description, isPublic, groups, pool }) {
|
||||
async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', description, isPublic, showCharacterNames = false, groups, pool }) {
|
||||
const existing = id ? await findTierListById(id, authorId) : null
|
||||
await syncOwnedCustomItemLabels({ ownerId: authorId, items: pool })
|
||||
const nextThumbnailSrc = (thumbnailSrc || '').trim() || getAutoThumbnailSrc(groups, pool)
|
||||
@@ -1223,10 +1242,10 @@ async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', de
|
||||
await query(
|
||||
`
|
||||
UPDATE tierlists
|
||||
SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, groups_json = ?, pool_json = ?, updated_at = ?
|
||||
SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, show_character_names = ?, groups_json = ?, pool_json = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, serializeJson(groups), serializeJson(pool), now(), existing.id]
|
||||
[title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, serializeJson(groups), serializeJson(pool), now(), existing.id]
|
||||
)
|
||||
return findTierListById(existing.id, authorId)
|
||||
}
|
||||
@@ -1235,11 +1254,11 @@ async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', de
|
||||
await query(
|
||||
`
|
||||
INSERT INTO tierlists (
|
||||
id, author_id, game_id, title, thumbnail_src, description, is_public, groups_json, pool_json, created_at, updated_at
|
||||
id, author_id, game_id, title, thumbnail_src, description, is_public, show_character_names, groups_json, pool_json, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[id, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, serializeJson(groups), serializeJson(pool), createdAt, createdAt]
|
||||
[id, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, serializeJson(groups), serializeJson(pool), createdAt, createdAt]
|
||||
)
|
||||
return findTierListById(id, authorId)
|
||||
}
|
||||
|
||||
@@ -61,6 +61,14 @@ const upload = multer({
|
||||
limits: { fileSize: 6 * 1024 * 1024 },
|
||||
})
|
||||
|
||||
const avatarUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'avatars')),
|
||||
filename: (req, file, cb) => cb(null, buildUploadFilename(file)),
|
||||
}),
|
||||
limits: { fileSize: 3 * 1024 * 1024 },
|
||||
})
|
||||
|
||||
router.post('/games', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({ id: z.string().min(1), name: z.string().min(1).max(60) })
|
||||
const parsed = schema.safeParse(req.body)
|
||||
@@ -494,6 +502,29 @@ router.patch('/users/:userId', requireAdmin, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/users/:userId/avatar', requireAdmin, avatarUpload.single('avatar'), async (req, res) => {
|
||||
const schema = z.object({
|
||||
removeAvatar: z.union([z.literal('1'), z.undefined()]).optional(),
|
||||
})
|
||||
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 shouldRemoveAvatar = parsed.data.removeAvatar === '1'
|
||||
const nextAvatarSrc = shouldRemoveAvatar ? '' : req.file ? `/uploads/avatars/${req.file.filename}` : user.avatarSrc || ''
|
||||
const updated = await adminUpdateUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
nickname: user.nickname || '',
|
||||
isAdmin: !!user.isAdmin,
|
||||
avatarSrc: nextAvatarSrc,
|
||||
})
|
||||
|
||||
res.json({ user: updated })
|
||||
})
|
||||
|
||||
router.delete('/users/:userId', requireAdmin, async (req, res) => {
|
||||
if (req.params.userId === req.session.userId) {
|
||||
return res.status(400).json({ error: 'cannot_delete_self' })
|
||||
|
||||
@@ -28,6 +28,7 @@ const signupSchema = z.object({
|
||||
|
||||
const profileSchema = z.object({
|
||||
nickname: z.string().trim().min(1).max(40),
|
||||
removeAvatar: z.union([z.string(), z.undefined()]).optional(),
|
||||
})
|
||||
|
||||
router.post('/signup', async (req, res) => {
|
||||
@@ -108,7 +109,12 @@ router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) =
|
||||
const user = await findUserById(req.session.userId)
|
||||
if (!user) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const nextAvatarSrc = req.file ? `/uploads/avatars/${req.file.filename}` : user.avatarSrc || ''
|
||||
const shouldRemoveAvatar = parsed.data.removeAvatar === '1'
|
||||
const nextAvatarSrc = shouldRemoveAvatar
|
||||
? ''
|
||||
: req.file
|
||||
? `/uploads/avatars/${req.file.filename}`
|
||||
: user.avatarSrc || ''
|
||||
const updated = await updateUserProfile({
|
||||
id: user.id,
|
||||
nickname: parsed.data.nickname,
|
||||
|
||||
@@ -83,6 +83,7 @@ const tierListUpsertSchema = z.object({
|
||||
thumbnailSrc: z.string().max(255).optional().default(''),
|
||||
description: z.string().max(1000).optional().default(''),
|
||||
isPublic: z.boolean().default(false),
|
||||
showCharacterNames: z.boolean().optional().default(false),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -244,6 +245,7 @@ router.post('/', requireAuth, async (req, res) => {
|
||||
thumbnailSrc: payload.thumbnailSrc || '',
|
||||
description: payload.description || '',
|
||||
isPublic: !!payload.isPublic,
|
||||
showCharacterNames: !!payload.showCharacterNames,
|
||||
groups: payload.groups,
|
||||
pool: normalizedPool,
|
||||
})
|
||||
@@ -258,6 +260,7 @@ router.post('/', requireAuth, async (req, res) => {
|
||||
thumbnailSrc: payload.thumbnailSrc || '',
|
||||
description: payload.description || '',
|
||||
isPublic: !!payload.isPublic,
|
||||
showCharacterNames: !!payload.showCharacterNames,
|
||||
groups: payload.groups,
|
||||
pool: normalizedPool,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user