folders.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import { Router } from 'express';
  2. import { prisma } from '../lib/prisma';
  3. import { authMiddleware } from '../lib/auth';
  4. const router = Router();
  5. const str = (v: string | string[] | undefined): string =>
  6. Array.isArray(v) ? (v[0] ?? '') : (v ?? '');
  7. async function getUserRole(
  8. userId: string,
  9. globalRole: string,
  10. projectId: string
  11. ): Promise<string | null> {
  12. if (globalRole === 'ADMIN') return 'ADMIN';
  13. const project = await prisma.project.findUnique({
  14. where: { id: projectId },
  15. select: { ownerId: true, members: { where: { userId }, select: { role: true } } },
  16. });
  17. if (!project) return null;
  18. if (project.ownerId === userId) return 'OWNER';
  19. return project.members[0]?.role ?? null;
  20. }
  21. function canManage(role: string | null): boolean {
  22. return role === 'ADMIN' || role === 'EDITOR' || role === 'OWNER';
  23. }
  24. // ── CORS preflight ────────────────────────────────────────────────────────────
  25. router.options('/', (_req, res) => { res.set('Access-Control-Allow-Origin', '*'); res.sendStatus(200); });
  26. router.options('/:id', (_req, res) => { res.set('Access-Control-Allow-Origin', '*'); res.sendStatus(200); });
  27. // ── GET /api/folders/project/:projectId ───────────────────────────────────────
  28. router.get('/project/:projectId', authMiddleware, async (req, res) => {
  29. try {
  30. const projectId = str(req.params.projectId);
  31. const role = await getUserRole(req.user!.userId, req.user!.globalRole, projectId);
  32. if (!role) { res.status(403).json({ error: 'Forbidden' }); return; }
  33. const folders = await prisma.folder.findMany({
  34. where: { projectId },
  35. orderBy: [{ order: 'asc' }, { name: 'asc' }],
  36. include: { _count: { select: { assets: true } } },
  37. });
  38. interface FolderNode { id: string; name: string; parentId: string | null; order: number; assetCount: number; assetIds: string[]; children: FolderNode[]; }
  39. interface FolderWithAssets extends FolderNode { _assetIds?: string[] }
  40. const foldersWithAssets: FolderNode[] = await Promise.all(
  41. folders.map(async (f) => {
  42. const fa = await prisma.folderAsset.findMany({ where: { folderId: f.id }, select: { assetId: true } });
  43. return { id: f.id, name: f.name, parentId: f.parentId, order: f.order, assetCount: f._count.assets, assetIds: fa.map(a => a.assetId), children: [] };
  44. })
  45. );
  46. const map = new Map<string, FolderNode>();
  47. for (const f of foldersWithAssets) map.set(f.id, f);
  48. const roots: FolderNode[] = [];
  49. for (const f of foldersWithAssets) {
  50. if (f.parentId && map.has(f.parentId)) {
  51. map.get(f.parentId)!.children.push(map.get(f.id)!);
  52. } else {
  53. roots.push(map.get(f.id)!);
  54. }
  55. }
  56. res.json({ folders: roots, allFolders: foldersWithAssets });
  57. } catch (err) {
  58. console.error('[folders] GET /project/:projectId error:', err);
  59. res.status(500).json({ error: 'Internal server error' });
  60. }
  61. });
  62. // ── POST /api/folders ─────────────────────────────────────────────────────────
  63. router.post('/', authMiddleware, async (req, res) => {
  64. try {
  65. const { name, projectId, parentId } = req.body as { name?: string; projectId?: string; parentId?: string };
  66. if (!name?.trim() || !projectId) { res.status(400).json({ error: 'name and projectId are required' }); return; }
  67. const role = await getUserRole(req.user!.userId, req.user!.globalRole, projectId);
  68. if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; }
  69. if (parentId) {
  70. const parent = await prisma.folder.findUnique({ where: { id: parentId } });
  71. if (!parent || parent.projectId !== projectId) { res.status(400).json({ error: 'Invalid parent folder' }); return; }
  72. }
  73. const siblings = await prisma.folder.findMany({
  74. where: { projectId, parentId: parentId ?? null },
  75. select: { order: true }, orderBy: { order: 'desc' }, take: 1,
  76. });
  77. const nextOrder = (siblings[0]?.order ?? -1) + 1;
  78. const created = await prisma.folder.create({
  79. data: { name: name.trim(), projectId, parentId: parentId ?? null, order: nextOrder },
  80. include: { _count: { select: { assets: true } } },
  81. });
  82. res.status(201).json({
  83. folder: { id: created.id, name: created.name, parentId: created.parentId, order: created.order, assetCount: created._count.assets, assetIds: [], children: [] },
  84. });
  85. } catch (err) {
  86. console.error('[folders] POST / error:', err);
  87. res.status(500).json({ error: 'Internal server error' });
  88. }
  89. });
  90. // ── PUT /api/folders/:id ──────────────────────────────────────────────────────
  91. router.put('/:id', authMiddleware, async (req, res) => {
  92. try {
  93. const id = str(req.params.id);
  94. const { name } = req.body as { name?: string };
  95. const existing = await prisma.folder.findUnique({ where: { id } });
  96. if (!existing) { res.status(404).json({ error: 'Folder not found' }); return; }
  97. const role = await getUserRole(req.user!.userId, req.user!.globalRole, existing.projectId);
  98. if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; }
  99. const folder = await prisma.folder.update({
  100. where: { id },
  101. data: name?.trim() ? { name: name.trim() } : {},
  102. select: { id: true, name: true, projectId: true, parentId: true, order: true },
  103. });
  104. res.json({ folder });
  105. } catch (err) {
  106. console.error('[folders] PUT /:id error:', err);
  107. res.status(500).json({ error: 'Internal server error' });
  108. }
  109. });
  110. // ── DELETE /api/folders/:id ────────────────────────────────────────────────────
  111. router.delete('/:id', authMiddleware, async (req, res) => {
  112. try {
  113. const id = str(req.params.id);
  114. const existing = await prisma.folder.findUnique({ where: { id } });
  115. if (!existing) { res.status(404).json({ error: 'Folder not found' }); return; }
  116. const role = await getUserRole(req.user!.userId, req.user!.globalRole, existing.projectId);
  117. if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; }
  118. await prisma.folder.delete({ where: { id } });
  119. res.json({ success: true });
  120. } catch (err) {
  121. console.error('[folders] DELETE /:id error:', err);
  122. res.status(500).json({ error: 'Internal server error' });
  123. }
  124. });
  125. // ── POST /api/folders/:id/assets ───────────────────────────────────────────────
  126. router.post('/:id/assets', authMiddleware, async (req, res) => {
  127. try {
  128. const id = str(req.params.id);
  129. const { assetIds } = req.body as { assetIds?: string[] };
  130. if (!Array.isArray(assetIds) || assetIds.length === 0) { res.status(400).json({ error: 'assetIds array is required' }); return; }
  131. const folder = await prisma.folder.findUnique({ where: { id } });
  132. if (!folder) { res.status(404).json({ error: 'Folder not found' }); return; }
  133. const role = await getUserRole(req.user!.userId, req.user!.globalRole, folder.projectId);
  134. if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; }
  135. const validAssets = await prisma.asset.findMany({
  136. where: { id: { in: assetIds }, projectId: folder.projectId },
  137. select: { id: true },
  138. });
  139. const validIds: string[] = validAssets.map((a: { id: string }) => a.id);
  140. await Promise.all(
  141. validIds.map((assetId: string) =>
  142. prisma.folderAsset.upsert({
  143. where: { folderId_assetId: { folderId: id, assetId } },
  144. create: { folderId: id, assetId },
  145. update: {},
  146. })
  147. )
  148. );
  149. const count = await prisma.folderAsset.count({ where: { folderId: id } });
  150. res.json({ success: true, assetCount: count });
  151. } catch (err) {
  152. console.error('[folders] POST /:id/assets error:', err);
  153. res.status(500).json({ error: 'Internal server error' });
  154. }
  155. });
  156. // ── PUT /api/folders/:id/assets ───────────────────────────────────────────────
  157. router.put('/:id/assets', authMiddleware, async (req, res) => {
  158. try {
  159. const id = str(req.params.id);
  160. const { assetIds } = req.body as { assetIds?: string[] };
  161. const folder = await prisma.folder.findUnique({ where: { id } });
  162. if (!folder) { res.status(404).json({ error: 'Folder not found' }); return; }
  163. const role = await getUserRole(req.user!.userId, req.user!.globalRole, folder.projectId);
  164. if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; }
  165. const validAssets = await prisma.asset.findMany({
  166. where: { id: { in: assetIds ?? [] }, projectId: folder.projectId },
  167. select: { id: true },
  168. });
  169. const validIds: string[] = validAssets.map((a: { id: string }) => a.id);
  170. await prisma.folderAsset.deleteMany({ where: { folderId: id } });
  171. if (validIds.length > 0) {
  172. await prisma.folderAsset.createMany({
  173. data: validIds.map((assetId: string) => ({ folderId: id, assetId })),
  174. skipDuplicates: true,
  175. });
  176. }
  177. res.json({ success: true, assetCount: validIds.length });
  178. } catch (err) {
  179. console.error('[folders] PUT /:id/assets error:', err);
  180. res.status(500).json({ error: 'Internal server error' });
  181. }
  182. });
  183. // ── DELETE /api/folders/:id/assets/:assetId ───────────────────────────────────
  184. router.delete('/:id/assets/:assetId', authMiddleware, async (req, res) => {
  185. try {
  186. const id = str(req.params.id);
  187. const assetId = str(req.params.assetId);
  188. const folder = await prisma.folder.findUnique({ where: { id } });
  189. if (!folder) { res.status(404).json({ error: 'Folder not found' }); return; }
  190. const role = await getUserRole(req.user!.userId, req.user!.globalRole, folder.projectId);
  191. if (!canManage(role)) { res.status(403).json({ error: 'Forbidden' }); return; }
  192. await prisma.folderAsset.deleteMany({ where: { folderId: id, assetId } });
  193. res.json({ success: true });
  194. } catch (err) {
  195. console.error('[folders] DELETE /:id/assets/:assetId error:', err);
  196. res.status(500).json({ error: 'Internal server error' });
  197. }
  198. });
  199. export default router;