feat: image upload & preview untuk bug reports via MinIO
- Upload hingga 3 gambar per bug report (FileInput multi-select) - Backend: POST /api/upload/image → MinIO, GET /api/bugs/images → presigned URL redirect - Auto-create bucket jika belum ada saat server start - Preview gambar fullscreen saat thumbnail diklik - Diterapkan di /bug-reports dan /apps/$appId/errors - Migrasi storage dari Seafile ke MinIO (minio SDK v8) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
55
src/app.ts
55
src/app.ts
@@ -6,6 +6,7 @@ import { BugSource } from '../generated/prisma'
|
||||
import { prisma } from './lib/db'
|
||||
import { env } from './lib/env'
|
||||
import { createSystemLog } from './lib/logger'
|
||||
import { getMinioDownloadUrl, uploadBugImage } from './lib/minio'
|
||||
|
||||
interface AuthResult {
|
||||
actingUserId: string
|
||||
@@ -624,8 +625,8 @@ export function createApp() {
|
||||
description: body.description,
|
||||
stackTrace: body.stackTrace,
|
||||
userId: reporterUserId,
|
||||
images: body.imageUrl ? {
|
||||
create: { imageUrl: body.imageUrl }
|
||||
images: body.imageUrls?.length ? {
|
||||
createMany: { data: body.imageUrls.map(imageUrl => ({ imageUrl })) }
|
||||
} : undefined,
|
||||
logs: {
|
||||
create: {
|
||||
@@ -649,7 +650,7 @@ export function createApp() {
|
||||
source: t.Optional(t.String({
|
||||
description: 'Sumber laporan: QC | SYSTEM | USER',
|
||||
})),
|
||||
imageUrl: t.Optional(t.String({ description: 'URL gambar screenshot bug (opsional)' })),
|
||||
imageUrls: t.Optional(t.Array(t.String(), { description: 'URL gambar screenshot bug, maks 3 (opsional)' })),
|
||||
}),
|
||||
detail: {
|
||||
summary: 'Create Bug Report',
|
||||
@@ -756,6 +757,54 @@ export function createApp() {
|
||||
},
|
||||
})
|
||||
|
||||
// ─── Image Upload & Proxy ──────────────────────────
|
||||
.post('/api/upload/image', async ({ body, request, set }) => {
|
||||
const auth = await checkAuth(request)
|
||||
if (!auth) {
|
||||
set.status = 401
|
||||
return { error: 'Unauthorized' }
|
||||
}
|
||||
const { file } = body
|
||||
if (!file.type.startsWith('image/')) {
|
||||
set.status = 400
|
||||
return { error: 'Hanya file gambar yang diperbolehkan' }
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
set.status = 400
|
||||
return { error: 'Ukuran file maksimal 5MB' }
|
||||
}
|
||||
const path = await uploadBugImage(file)
|
||||
return { url: `/api/bugs/images?path=${encodeURIComponent(path)}` }
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.File({ description: 'File gambar screenshot bug (maks 5MB)' }),
|
||||
}),
|
||||
detail: {
|
||||
summary: 'Upload Bug Image',
|
||||
description: 'Upload gambar screenshot bug ke MinIO. Mengembalikan URL proxy yang dapat langsung dipakai sebagai imageUrl pada POST /api/bugs.',
|
||||
tags: ['Bugs'],
|
||||
},
|
||||
})
|
||||
|
||||
.get('/api/bugs/images', async ({ query, set }) => {
|
||||
try {
|
||||
const downloadUrl = await getMinioDownloadUrl(query.path)
|
||||
return new Response(null, { status: 302, headers: { Location: downloadUrl } })
|
||||
} catch {
|
||||
set.status = 404
|
||||
return { error: 'Gambar tidak ditemukan' }
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
path: t.String({ description: 'Path file di Seafile (contoh: /bug-reports/uuid.jpg)' }),
|
||||
}),
|
||||
detail: {
|
||||
summary: 'Get Bug Image',
|
||||
description: 'Proxy gambar bug dari MinIO. Meredirect ke presigned download URL MinIO (valid 1 jam).',
|
||||
tags: ['Bugs'],
|
||||
},
|
||||
})
|
||||
|
||||
// ─── System Status API ─────────────────────────────
|
||||
.get('/api/system/status', async () => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user