forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { count, eq } from 'drizzle-orm'
|
||||
import { comments, uploads, users } from '../../db/schema'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = getRouterParam(event, 'id') ?? ''
|
||||
if (!/^\d+$/.test(idParam)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Nutzer-ID' })
|
||||
}
|
||||
const userId = Number(idParam)
|
||||
|
||||
const db = useDb()
|
||||
const [user] = await db
|
||||
.select({ id: users.id, name: users.name, color: users.color, avatar: users.avatar, cardEffect: users.cardEffect, cardEffectColor: users.cardEffectColor, cardVignette: users.cardVignette, cardBorderEffect: users.cardBorderEffect, cardBorderEffectColor: users.cardBorderEffectColor, createdAt: users.createdAt })
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1)
|
||||
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Nutzer nicht gefunden' })
|
||||
}
|
||||
|
||||
const [uploadStats] = await db
|
||||
.select({ count: count() })
|
||||
.from(uploads)
|
||||
.where(eq(uploads.userId, user.id))
|
||||
|
||||
const [commentStats] = await db
|
||||
.select({ count: count() })
|
||||
.from(comments)
|
||||
.where(eq(comments.userId, user.id))
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
color: user.color,
|
||||
avatarUrl: avatarUrl(user.avatar),
|
||||
cardEffect: user.cardEffect,
|
||||
cardEffectColor: user.cardEffectColor, cardVignette: user.cardVignette,
|
||||
cardBorderEffect: user.cardBorderEffect,
|
||||
cardBorderEffectColor: user.cardBorderEffectColor,
|
||||
createdAt: user.createdAt,
|
||||
},
|
||||
stats: {
|
||||
uploads: uploadStats?.count ?? 0,
|
||||
comments: commentStats?.count ?? 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { asc, count, desc, eq, inArray } from 'drizzle-orm'
|
||||
import { commentAttachments, comments, uploads, users } from '../../../db/schema'
|
||||
import type { CommentAttachment } from '~~/shared/utils/attachments'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = getRouterParam(event, 'id') ?? ''
|
||||
if (!/^\d+$/.test(idParam)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Nutzer-ID' })
|
||||
}
|
||||
const userId = Number(idParam)
|
||||
|
||||
const q = getQuery(event)
|
||||
const page = Math.max(1, Math.floor(Number(q.page)) || 1)
|
||||
const perPage = Math.min(100, Math.max(1, Math.floor(Number(q.perPage)) || 20))
|
||||
|
||||
const db = useDb()
|
||||
const [user] = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1)
|
||||
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Nutzer nicht gefunden' })
|
||||
}
|
||||
|
||||
const [totalRow, rows] = await Promise.all([
|
||||
db.select({ count: count() }).from(comments).where(eq(comments.userId, userId)),
|
||||
db
|
||||
.select({
|
||||
id: comments.id,
|
||||
content: comments.content,
|
||||
createdAt: comments.createdAt,
|
||||
uploadId: uploads.id,
|
||||
uploadType: uploads.type,
|
||||
uploadFileName: uploads.fileName,
|
||||
uploadExternalUrl: uploads.externalUrl,
|
||||
uploadPreviewImage: uploads.previewImage,
|
||||
})
|
||||
.from(comments)
|
||||
.innerJoin(uploads, eq(comments.uploadId, uploads.id))
|
||||
.where(eq(comments.userId, userId))
|
||||
.orderBy(desc(comments.createdAt))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
])
|
||||
|
||||
const total = totalRow[0]?.count ?? 0
|
||||
|
||||
const commentIds = rows.map(r => r.id)
|
||||
const attachmentRows = commentIds.length
|
||||
? await db
|
||||
.select()
|
||||
.from(commentAttachments)
|
||||
.where(inArray(commentAttachments.commentId, commentIds))
|
||||
.orderBy(asc(commentAttachments.id))
|
||||
: []
|
||||
const attachmentsByComment = new Map<number, CommentAttachment[]>()
|
||||
for (const a of attachmentRows) {
|
||||
let list = attachmentsByComment.get(a.commentId)
|
||||
if (!list) {
|
||||
list = []
|
||||
attachmentsByComment.set(a.commentId, list)
|
||||
}
|
||||
list.push({ id: a.id, name: a.originalName, url: `/uploads/attachments/${a.fileName}`, mimeType: a.mimeType, size: a.size, cover: a.cover, meta: a.meta as CommentAttachment['meta'] })
|
||||
}
|
||||
|
||||
const items = rows.map((row) => {
|
||||
const url = row.uploadExternalUrl ?? `/uploads/${row.uploadFileName}`
|
||||
const embedId = row.uploadType === 'youtube' && row.uploadExternalUrl ? getYoutubeId(row.uploadExternalUrl) : null
|
||||
const uploadThumbUrl = row.uploadType === 'youtube' && embedId
|
||||
? youtubeThumbUrl(embedId)
|
||||
: row.uploadType === 'image' ? url : row.uploadType === 'link' || row.uploadType === 'twitch' || row.uploadType === 'audio' ? row.uploadPreviewImage : null
|
||||
return {
|
||||
id: row.id,
|
||||
content: row.content,
|
||||
createdAt: row.createdAt,
|
||||
uploadId: row.uploadId,
|
||||
uploadThumbUrl,
|
||||
attachments: attachmentsByComment.get(row.id) ?? [],
|
||||
}
|
||||
})
|
||||
|
||||
return { items, total, pageCount: Math.max(1, Math.ceil(total / perPage)) }
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { count, eq } from 'drizzle-orm'
|
||||
import { comments, uploads, users } from '../../../db/schema'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = getRouterParam(event, 'id') ?? ''
|
||||
if (!/^\d+$/.test(idParam)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Nutzer-ID' })
|
||||
}
|
||||
const userId = Number(idParam)
|
||||
|
||||
const db = useDb()
|
||||
const [user] = await db
|
||||
.select({ id: users.id, name: users.name, color: users.color, avatar: users.avatar, cardEffect: users.cardEffect, cardEffectColor: users.cardEffectColor, cardVignette: users.cardVignette, cardBorderEffect: users.cardBorderEffect, cardBorderEffectColor: users.cardBorderEffectColor, createdAt: users.createdAt })
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1)
|
||||
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Nutzer nicht gefunden' })
|
||||
}
|
||||
|
||||
const [uploadStats] = await db
|
||||
.select({ count: count() })
|
||||
.from(uploads)
|
||||
.where(eq(uploads.userId, user.id))
|
||||
|
||||
const [commentStats] = await db
|
||||
.select({ count: count() })
|
||||
.from(comments)
|
||||
.where(eq(comments.userId, user.id))
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
color: user.color,
|
||||
cardEffect: user.cardEffect,
|
||||
cardEffectColor: user.cardEffectColor, cardVignette: user.cardVignette,
|
||||
cardBorderEffect: user.cardBorderEffect,
|
||||
cardBorderEffectColor: user.cardBorderEffectColor,
|
||||
avatarUrl: avatarUrl(user.avatar),
|
||||
createdAt: user.createdAt,
|
||||
stats: {
|
||||
uploads: uploadStats?.count ?? 0,
|
||||
comments: commentStats?.count ?? 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
import { uploads, users } from '../../../db/schema'
|
||||
|
||||
// Upload-Liste eines Nutzers mit Viewport-Pagination (wie /api/uploads).
|
||||
// thumbUrl wird hier serverseitig mitgeliefert — kein separater Meta-Call.
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = getRouterParam(event, 'id') ?? ''
|
||||
if (!/^\d+$/.test(idParam)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Nutzer-ID' })
|
||||
}
|
||||
const userId = Number(idParam)
|
||||
|
||||
const q = getQuery(event)
|
||||
const { page, cells, cols, uniform } = getGridPageParams(q)
|
||||
|
||||
const db = useDb()
|
||||
const rows = await db
|
||||
.select({
|
||||
id: uploads.id,
|
||||
type: uploads.type,
|
||||
fileName: uploads.fileName,
|
||||
externalUrl: uploads.externalUrl,
|
||||
previewImage: uploads.previewImage,
|
||||
commentCount: uploads.commentCount,
|
||||
createdAt: uploads.createdAt,
|
||||
userName: users.name,
|
||||
userColor: users.color,
|
||||
userAvatar: users.avatar,
|
||||
})
|
||||
.from(uploads)
|
||||
.innerJoin(users, eq(uploads.userId, users.id))
|
||||
.where(eq(uploads.userId, userId))
|
||||
.orderBy(desc(uploads.createdAt))
|
||||
|
||||
const pages = paginateGrid(rows, r => gridTileDims(r.commentCount, uniform, cols), cells, cols)
|
||||
|
||||
const items = (pages[page - 1] ?? []).map((row) => {
|
||||
const url = row.externalUrl ?? `/uploads/${row.fileName}`
|
||||
const embedId = row.type === 'youtube' && row.externalUrl ? getYoutubeId(row.externalUrl) : null
|
||||
const thumbUrl = row.type === 'youtube' && embedId
|
||||
? youtubeThumbUrl(embedId)
|
||||
: row.type === 'image' ? url : row.type === 'link' || row.type === 'twitch' || row.type === 'audio' ? row.previewImage : null
|
||||
return { ...row, url, thumbUrl, userAvatarUrl: avatarUrl(row.userAvatar) }
|
||||
})
|
||||
|
||||
return { items, total: rows.length, pageCount: pages.length }
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { asc, count } from 'drizzle-orm'
|
||||
import { comments, uploads, users } from '../../db/schema'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const db = useDb()
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
color: users.color,
|
||||
avatar: users.avatar,
|
||||
cardEffect: users.cardEffect,
|
||||
cardEffectColor: users.cardEffectColor, cardVignette: users.cardVignette,
|
||||
cardBorderEffect: users.cardBorderEffect,
|
||||
cardBorderEffectColor: users.cardBorderEffectColor,
|
||||
createdAt: users.createdAt,
|
||||
})
|
||||
.from(users)
|
||||
.orderBy(asc(users.id))
|
||||
|
||||
const [uploadStats, commentStats] = await Promise.all([
|
||||
db.select({ userId: uploads.userId, count: count() }).from(uploads).groupBy(uploads.userId),
|
||||
db.select({ userId: comments.userId, count: count() }).from(comments).groupBy(comments.userId),
|
||||
])
|
||||
const uploadCount = new Map(uploadStats.map(s => [s.userId, s.count]))
|
||||
const commentCount = new Map(commentStats.map(s => [s.userId, s.count]))
|
||||
|
||||
return rows.map(u => ({
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
color: u.color,
|
||||
avatarUrl: avatarUrl(u.avatar),
|
||||
cardEffect: u.cardEffect,
|
||||
cardEffectColor: u.cardEffectColor, cardVignette: u.cardVignette,
|
||||
cardBorderEffect: u.cardBorderEffect,
|
||||
cardBorderEffectColor: u.cardBorderEffectColor,
|
||||
createdAt: u.createdAt,
|
||||
stats: {
|
||||
uploads: uploadCount.get(u.id) ?? 0,
|
||||
comments: commentCount.get(u.id) ?? 0,
|
||||
},
|
||||
}))
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, unlink, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { isCardBorderEffect } from '../../../shared/utils/cardBorderEffects'
|
||||
import { isCardEffect } from '../../../shared/utils/cardEffects'
|
||||
import { users } from '../../db/schema'
|
||||
|
||||
const AVATAR_MIME_EXT: Record<string, string> = {
|
||||
'image/jpeg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/gif': '.gif',
|
||||
'image/webp': '.webp',
|
||||
'image/avif': '.avif',
|
||||
}
|
||||
|
||||
const MAX_AVATAR_SIZE = 5 * 1024 * 1024 // 5 MB
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await requireUser(event)
|
||||
const parts = await readMultipartFormData(event)
|
||||
|
||||
const colorField = parts?.find(p => p.name === 'color')?.data.toString('utf8').trim()
|
||||
const cardEffectField = parts?.find(p => p.name === 'cardEffect')?.data.toString('utf8').trim()
|
||||
const cardEffectColorField = parts?.find(p => p.name === 'cardEffectColor')?.data.toString('utf8').trim()
|
||||
const cardVignetteField = parts?.find(p => p.name === 'cardVignette')?.data.toString('utf8').trim()
|
||||
const cardBorderEffectField = parts?.find(p => p.name === 'cardBorderEffect')?.data.toString('utf8').trim()
|
||||
const cardBorderEffectColorField = parts?.find(p => p.name === 'cardBorderEffectColor')?.data.toString('utf8').trim()
|
||||
const avatarFile = parts?.find(p => p.name === 'avatar')
|
||||
|
||||
let color: string | null | undefined
|
||||
if (colorField !== undefined) {
|
||||
if (colorField === '') {
|
||||
color = null
|
||||
}
|
||||
else if (/^#[0-9a-fA-F]{6}$/.test(colorField)) {
|
||||
color = colorField.toLowerCase()
|
||||
}
|
||||
else {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Farbe' })
|
||||
}
|
||||
}
|
||||
|
||||
let cardEffect: string | null | undefined
|
||||
if (cardEffectField !== undefined) {
|
||||
if (cardEffectField === '') {
|
||||
cardEffect = null
|
||||
}
|
||||
else if (isCardEffect(cardEffectField)) {
|
||||
cardEffect = cardEffectField
|
||||
}
|
||||
else {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültiger Effekt' })
|
||||
}
|
||||
}
|
||||
|
||||
let cardBorderEffect: string | null | undefined
|
||||
if (cardBorderEffectField !== undefined) {
|
||||
if (cardBorderEffectField === '') {
|
||||
cardBorderEffect = null
|
||||
}
|
||||
else if (isCardBorderEffect(cardBorderEffectField)) {
|
||||
cardBorderEffect = cardBorderEffectField
|
||||
}
|
||||
else {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültiger Border-Effekt' })
|
||||
}
|
||||
}
|
||||
|
||||
const parseOptionalColor = (field: string | undefined): string | null | undefined => {
|
||||
if (field === undefined) return undefined
|
||||
if (field === '') return null
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(field)) return field.toLowerCase()
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Effekt-Farbe' })
|
||||
}
|
||||
const cardEffectColor = parseOptionalColor(cardEffectColorField)
|
||||
const cardBorderEffectColor = parseOptionalColor(cardBorderEffectColorField)
|
||||
const cardVignette = cardVignetteField === undefined ? undefined : cardVignetteField === '1'
|
||||
|
||||
let avatarName: string | undefined
|
||||
if (avatarFile?.data?.length && avatarFile.type) {
|
||||
const ext = AVATAR_MIME_EXT[avatarFile.type]
|
||||
if (!ext) {
|
||||
throw createError({ statusCode: 415, statusMessage: 'Nur Bilder sind als Avatar erlaubt' })
|
||||
}
|
||||
if (avatarFile.data.length > MAX_AVATAR_SIZE) {
|
||||
throw createError({ statusCode: 413, statusMessage: 'Avatar ist zu groß (max. 5 MB)' })
|
||||
}
|
||||
avatarName = `${randomUUID()}${ext}`
|
||||
await mkdir(avatarDir(), { recursive: true })
|
||||
await writeFile(join(avatarDir(), avatarName), avatarFile.data)
|
||||
}
|
||||
|
||||
const db = useDb()
|
||||
await db.update(users).set({
|
||||
...(color !== undefined && { color }),
|
||||
...(cardEffect !== undefined && { cardEffect }),
|
||||
...(cardEffectColor !== undefined && { cardEffectColor }),
|
||||
...(cardVignette !== undefined && { cardVignette }),
|
||||
...(cardBorderEffect !== undefined && { cardBorderEffect }),
|
||||
...(cardBorderEffectColor !== undefined && { cardBorderEffectColor }),
|
||||
...(avatarName && { avatar: avatarName }),
|
||||
}).where(eq(users.id, user.id))
|
||||
|
||||
if (avatarName && user.avatar) {
|
||||
await unlink(join(avatarDir(), user.avatar)).catch(() => {})
|
||||
}
|
||||
|
||||
const [updated] = await db.select().from(users).where(eq(users.id, user.id)).limit(1)
|
||||
return {
|
||||
user: {
|
||||
id: updated!.id,
|
||||
name: updated!.name,
|
||||
email: updated!.email,
|
||||
color: updated!.color,
|
||||
cardEffect: updated!.cardEffect,
|
||||
cardEffectColor: updated!.cardEffectColor,
|
||||
cardVignette: updated!.cardVignette,
|
||||
cardBorderEffect: updated!.cardBorderEffect,
|
||||
cardBorderEffectColor: updated!.cardBorderEffectColor,
|
||||
avatarUrl: avatarUrl(updated!.avatar),
|
||||
roles: user.roles,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { asc } from 'drizzle-orm'
|
||||
import { users } from '../../db/schema'
|
||||
|
||||
export default defineEventHandler(() => useDb()
|
||||
.select({ id: users.id, name: users.name, color: users.color })
|
||||
.from(users)
|
||||
.orderBy(asc(users.name)))
|
||||
Reference in New Issue
Block a user