import { Router } from 'express'; import { prisma } from '../lib/prisma'; import { authMiddleware } from '../lib/auth'; const router = Router(); const str = (v: string | string[] | undefined): string => Array.isArray(v) ? (v[0] ?? '') : (v ?? ''); async function getUserRole( userId: string, globalRole: string, projectId: string ): Promise { if (globalRole === 'ADMIN') return 'ADMIN'; const project = await prisma.project.findUnique({ where: { id: projectId }, select: { ownerId: true, members: { where: { userId }, select: { role: true } } }, }); if (!project) return null; if (project.ownerId === userId) return 'OWNER'; return project.members[0]?.role ?? null; } function canManage(role: string | null): boolean { return role === 'ADMIN' || role === 'EDITOR' || role === 'OWNER'; } // ── CORS preflight ──────────────────────────────────────────────────────────── router.options('/', (_req, res) => { res.set('Access-Control-Allow-Origin', '*'); res.sendStatus(200); }); router.options('/:id', (_req, res) => { res.set('Access-Control-Allow-Origin', '*'); res.sendStatus(200); }); // ── GET /api/folders/project/:projectId ─────────────────────────────────────── router.get('/project/:projectId', authMiddleware, async (req, res) => { try { const projectId = str(req.params.projectId); const role = await getUserRole(req.user!.userId, req.user!.globalRole, projectId); if (!role) { res.status(403).json({ error: 'Forbidden' }); return; } const folders = await prisma.folder.findMany({ where: { projectId }, orderBy: [{ order: 'asc' }, { name: 'asc' }], include: { _count: { select: { assets: true } } }, }); interface FolderNode { id: string; name: string; parentId: string | null; order: number; assetCount: number; assetIds: string[]; children: FolderNode[]; } interface FolderWithAssets extends FolderNode { _assetIds?: string[] } const foldersWithAssets: FolderNode[] = await Promise.all( folders.map(async (f) => { const fa = await prisma.folderAsset.findMany({ where: { folderId: f.id }, select: { assetId: true } }); return { id: f.id, name: f.name, parentId: f.parentId, order: f.order, assetCount: f._count.assets, assetIds: fa.map(a => a.assetId), children: [] }; }) ); const map = new Map(); for (const f of foldersWithAssets) map.set(f.id, f); const roots: FolderNode[] = []; for (const f of foldersWithAssets) { if (f.parentId && map.has(f.parentId)) { map.get(f.parentId)!.children.push(map.get(f.id)!); } else { roots.push(map.get(f.id)!); } } res.json({ folders: roots, allFolders: foldersWithAssets }); } catch (err) { console.error('[folders] GET /project/:projectId error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // ── POST /api/folders ───────────────────────────────────────────────────────── router.post('/', authMiddleware, async (req, res) => { try { const { name, projectId, parentId } = req.body as { name?: string; projectId?: string; parentId?: string }; if (!name?.trim() || !projectId) { res.status(400).json({ error: 'name and projectId are required' }); return; } const role = await getUserRole(req.user!.userId, req.user!.globalRole, projectId); if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; } if (parentId) { const parent = await prisma.folder.findUnique({ where: { id: parentId } }); if (!parent || parent.projectId !== projectId) { res.status(400).json({ error: 'Invalid parent folder' }); return; } } const siblings = await prisma.folder.findMany({ where: { projectId, parentId: parentId ?? null }, select: { order: true }, orderBy: { order: 'desc' }, take: 1, }); const nextOrder = (siblings[0]?.order ?? -1) + 1; const created = await prisma.folder.create({ data: { name: name.trim(), projectId, parentId: parentId ?? null, order: nextOrder }, include: { _count: { select: { assets: true } } }, }); res.status(201).json({ folder: { id: created.id, name: created.name, parentId: created.parentId, order: created.order, assetCount: created._count.assets, assetIds: [], children: [] }, }); } catch (err) { console.error('[folders] POST / error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // ── PUT /api/folders/:id ────────────────────────────────────────────────────── router.put('/:id', authMiddleware, async (req, res) => { try { const id = str(req.params.id); const { name } = req.body as { name?: string }; const existing = await prisma.folder.findUnique({ where: { id } }); if (!existing) { res.status(404).json({ error: 'Folder not found' }); return; } const role = await getUserRole(req.user!.userId, req.user!.globalRole, existing.projectId); if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; } const folder = await prisma.folder.update({ where: { id }, data: name?.trim() ? { name: name.trim() } : {}, select: { id: true, name: true, projectId: true, parentId: true, order: true }, }); res.json({ folder }); } catch (err) { console.error('[folders] PUT /:id error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // ── DELETE /api/folders/:id ──────────────────────────────────────────────────── router.delete('/:id', authMiddleware, async (req, res) => { try { const id = str(req.params.id); const existing = await prisma.folder.findUnique({ where: { id } }); if (!existing) { res.status(404).json({ error: 'Folder not found' }); return; } const role = await getUserRole(req.user!.userId, req.user!.globalRole, existing.projectId); if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; } await prisma.folder.delete({ where: { id } }); res.json({ success: true }); } catch (err) { console.error('[folders] DELETE /:id error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // ── POST /api/folders/:id/assets ─────────────────────────────────────────────── router.post('/:id/assets', authMiddleware, async (req, res) => { try { const id = str(req.params.id); const { assetIds } = req.body as { assetIds?: string[] }; if (!Array.isArray(assetIds) || assetIds.length === 0) { res.status(400).json({ error: 'assetIds array is required' }); return; } const folder = await prisma.folder.findUnique({ where: { id } }); if (!folder) { res.status(404).json({ error: 'Folder not found' }); return; } const role = await getUserRole(req.user!.userId, req.user!.globalRole, folder.projectId); if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; } const validAssets = await prisma.asset.findMany({ where: { id: { in: assetIds }, projectId: folder.projectId }, select: { id: true }, }); const validIds: string[] = validAssets.map((a: { id: string }) => a.id); await Promise.all( validIds.map((assetId: string) => prisma.folderAsset.upsert({ where: { folderId_assetId: { folderId: id, assetId } }, create: { folderId: id, assetId }, update: {}, }) ) ); const count = await prisma.folderAsset.count({ where: { folderId: id } }); res.json({ success: true, assetCount: count }); } catch (err) { console.error('[folders] POST /:id/assets error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // ── PUT /api/folders/:id/assets ─────────────────────────────────────────────── router.put('/:id/assets', authMiddleware, async (req, res) => { try { const id = str(req.params.id); const { assetIds } = req.body as { assetIds?: string[] }; const folder = await prisma.folder.findUnique({ where: { id } }); if (!folder) { res.status(404).json({ error: 'Folder not found' }); return; } const role = await getUserRole(req.user!.userId, req.user!.globalRole, folder.projectId); if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; } const validAssets = await prisma.asset.findMany({ where: { id: { in: assetIds ?? [] }, projectId: folder.projectId }, select: { id: true }, }); const validIds: string[] = validAssets.map((a: { id: string }) => a.id); await prisma.folderAsset.deleteMany({ where: { folderId: id } }); if (validIds.length > 0) { await prisma.folderAsset.createMany({ data: validIds.map((assetId: string) => ({ folderId: id, assetId })), skipDuplicates: true, }); } res.json({ success: true, assetCount: validIds.length }); } catch (err) { console.error('[folders] PUT /:id/assets error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // ── DELETE /api/folders/:id/assets/:assetId ─────────────────────────────────── router.delete('/:id/assets/:assetId', authMiddleware, async (req, res) => { try { const id = str(req.params.id); const assetId = str(req.params.assetId); const folder = await prisma.folder.findUnique({ where: { id } }); if (!folder) { res.status(404).json({ error: 'Folder not found' }); return; } const role = await getUserRole(req.user!.userId, req.user!.globalRole, folder.projectId); if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; } await prisma.folderAsset.deleteMany({ where: { folderId: id, assetId } }); res.json({ success: true }); } catch (err) { console.error('[folders] DELETE /:id/assets/:assetId error:', err); res.status(500).json({ error: 'Internal server error' }); } }); export default router;