import { Router } from 'express'; import { z } from 'zod'; import { requireAuth } from '../middleware/auth'; import { ensureMinioBucket, minioBucket, minioClient, minioPresignedExpirySeconds, } from '../utils/minio'; const router = Router(); const uploadUrlSchema = z.object({ fileName: z.string().trim().min(1).max(255), prefix: z.string().trim().optional(), }); const downloadUrlSchema = z.object({ objectKey: z.string().trim().min(1), }); const listSchema = z.object({ prefix: z.string().trim().optional(), limit: z.coerce.number().int().min(1).max(100).default(20), }); const sanitizeSegment = (value: string): string => value.replace(/[^a-zA-Z0-9._/-]/g, '_'); const buildObjectKey = (userId: string, fileName: string, prefix?: string): string => { const safePrefix = prefix ? sanitizeSegment(prefix).replace(/^\/+|\/+$/g, '') : 'uploads'; const safeFileName = sanitizeSegment(fileName); return `${safePrefix}/${userId}/${Date.now()}-${safeFileName}`; }; router.use(requireAuth); router.post('/upload-url', async (req, res) => { const parsed = uploadUrlSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() }); return; } const user = req.user; if (!user) { res.status(401).json({ message: 'Unauthorized' }); return; } await ensureMinioBucket(); const objectKey = buildObjectKey(user.userId, parsed.data.fileName, parsed.data.prefix); const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds); res.status(201).json({ message: 'Dummy upload URL generated', bucket: minioBucket, objectKey, uploadUrl, expiresInSeconds: minioPresignedExpirySeconds, }); }); router.get('/download-url', async (req, res) => { const parsed = downloadUrlSchema.safeParse(req.query); if (!parsed.success) { res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() }); return; } await ensureMinioBucket(); const downloadUrl = await minioClient.presignedGetObject( minioBucket, parsed.data.objectKey, minioPresignedExpirySeconds, ); res.json({ message: 'Dummy download URL generated', bucket: minioBucket, objectKey: parsed.data.objectKey, downloadUrl, expiresInSeconds: minioPresignedExpirySeconds, }); }); router.get('/', async (req, res) => { const parsed = listSchema.safeParse(req.query); if (!parsed.success) { res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() }); return; } await ensureMinioBucket(); const objects = await new Promise< { objectKey: string | undefined; size: number; etag: string | undefined; lastModified: Date | undefined }[] >((resolve, reject) => { const results: { objectKey: string | undefined; size: number; etag: string | undefined; lastModified: Date | undefined; }[] = []; const stream = minioClient.listObjectsV2(minioBucket, parsed.data.prefix, true); stream.on('data', (item) => { if (results.length >= parsed.data.limit) { stream.destroy(); return; } results.push({ objectKey: item.name, size: item.size, etag: item.etag, lastModified: item.lastModified, }); }); stream.on('error', (error) => reject(error)); stream.on('end', () => resolve(results)); stream.on('close', () => resolve(results)); }); res.json({ bucket: minioBucket, count: objects.length, objects, }); }); router.delete('/', async (req, res) => { const parsed = downloadUrlSchema.safeParse(req.query); if (!parsed.success) { res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() }); return; } await ensureMinioBucket(); await minioClient.removeObject(minioBucket, parsed.data.objectKey); res.json({ message: 'Object deleted', bucket: minioBucket, objectKey: parsed.data.objectKey }); }); export default router;