| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- 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<string | null> {
- 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<string, FolderNode>();
- 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;
|