|
|
@@ -514,42 +514,89 @@ router.delete('/:id', async (req: Request, res: Response) => {
|
|
|
|
|
|
// ── Admin routes (registered before /:id so /admin/* is never shadowed) ────
|
|
|
|
|
|
-// GET /api/assets/admin/stuck-count — lightweight workspace-wide stuck job count
|
|
|
+// GET /api/assets/admin/stuck-count — count stuck jobs (workspace-wide or per-project)
|
|
|
+// Allowed for: global ADMIN, or project ADMIN/EDITOR
|
|
|
router.get('/admin/stuck-count', async (req: Request, res: Response) => {
|
|
|
try {
|
|
|
- if (req.user!.globalRole !== 'ADMIN') {
|
|
|
- res.status(403).json({ error: 'Admin access required' });
|
|
|
- return;
|
|
|
+ const { projectId } = req.query as { projectId?: string };
|
|
|
+ const isGlobalAdmin = req.user!.globalRole === 'ADMIN';
|
|
|
+ if (projectId) {
|
|
|
+ // Project-scoped: also allow project ADMIN/EDITOR
|
|
|
+ const membership = await prisma.projectMember.findFirst({
|
|
|
+ where: { projectId, userId: req.user!.userId, role: { in: ['ADMIN', 'EDITOR'] } },
|
|
|
+ });
|
|
|
+ if (!isGlobalAdmin && !membership) {
|
|
|
+ res.status(403).json({ error: 'Forbidden' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const count = await prisma.asset.count({
|
|
|
+ where: { projectId, transcodeStatus: 'PROCESSING', transcodePaused: false },
|
|
|
+ });
|
|
|
+ res.json({ count });
|
|
|
+ } else {
|
|
|
+ // Workspace-wide: global ADMIN only
|
|
|
+ if (!isGlobalAdmin) {
|
|
|
+ res.status(403).json({ error: 'Admin access required' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const count = await prisma.asset.count({
|
|
|
+ where: { transcodeStatus: 'PROCESSING', transcodePaused: false },
|
|
|
+ });
|
|
|
+ res.json({ count });
|
|
|
}
|
|
|
- const count = await prisma.asset.count({
|
|
|
- where: { transcodeStatus: 'PROCESSING', transcodePaused: false },
|
|
|
- });
|
|
|
- res.json({ count });
|
|
|
} catch (err) {
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
}
|
|
|
});
|
|
|
|
|
|
-// POST /api/assets/admin/reprocess-all — admin-only: reset all PROCESSING jobs to PENDING
|
|
|
+// POST /api/assets/admin/reprocess-all — reset stuck jobs (workspace-wide or per-project)
|
|
|
+// Allowed for: global ADMIN, or project ADMIN/EDITOR
|
|
|
router.post('/admin/reprocess-all', async (req: Request, res: Response) => {
|
|
|
try {
|
|
|
- if (req.user!.globalRole !== 'ADMIN') {
|
|
|
- res.status(403).json({ error: 'Admin access required' });
|
|
|
- return;
|
|
|
- }
|
|
|
- const stuck = await prisma.asset.findMany({
|
|
|
- where: { transcodeStatus: 'PROCESSING', transcodePaused: false },
|
|
|
- select: { id: true },
|
|
|
- });
|
|
|
- if (stuck.length === 0) {
|
|
|
- res.json({ message: 'No stuck jobs found', count: 0 });
|
|
|
- return;
|
|
|
+ const { projectId } = req.body as { projectId?: string };
|
|
|
+ const isGlobalAdmin = req.user!.globalRole === 'ADMIN';
|
|
|
+ if (projectId) {
|
|
|
+ // Project-scoped: also allow project ADMIN/EDITOR
|
|
|
+ const membership = await prisma.projectMember.findFirst({
|
|
|
+ where: { projectId, userId: req.user!.userId, role: { in: ['ADMIN', 'EDITOR'] } },
|
|
|
+ });
|
|
|
+ if (!isGlobalAdmin && !membership) {
|
|
|
+ res.status(403).json({ error: 'Forbidden' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const stuck = await prisma.asset.findMany({
|
|
|
+ where: { projectId, transcodeStatus: 'PROCESSING', transcodePaused: false },
|
|
|
+ select: { id: true },
|
|
|
+ });
|
|
|
+ if (stuck.length === 0) {
|
|
|
+ res.json({ message: 'No stuck jobs found', count: 0 });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await prisma.asset.updateMany({
|
|
|
+ where: { id: { in: stuck.map(s => s.id) } },
|
|
|
+ data: { transcodeStatus: 'PENDING', transcodeProgress: 0 },
|
|
|
+ });
|
|
|
+ res.json({ message: `Reset ${stuck.length} stuck job(s) to PENDING`, count: stuck.length });
|
|
|
+ } else {
|
|
|
+ // Workspace-wide: global ADMIN only
|
|
|
+ if (!isGlobalAdmin) {
|
|
|
+ res.status(403).json({ error: 'Admin access required' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const stuck = await prisma.asset.findMany({
|
|
|
+ where: { transcodeStatus: 'PROCESSING', transcodePaused: false },
|
|
|
+ select: { id: true },
|
|
|
+ });
|
|
|
+ if (stuck.length === 0) {
|
|
|
+ res.json({ message: 'No stuck jobs found', count: 0 });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await prisma.asset.updateMany({
|
|
|
+ where: { id: { in: stuck.map(s => s.id) } },
|
|
|
+ data: { transcodeStatus: 'PENDING', transcodeProgress: 0 },
|
|
|
+ });
|
|
|
+ res.json({ message: `Reset ${stuck.length} stuck job(s) to PENDING`, count: stuck.length });
|
|
|
}
|
|
|
- await prisma.asset.updateMany({
|
|
|
- where: { id: { in: stuck.map(s => s.id) } },
|
|
|
- data: { transcodeStatus: 'PENDING', transcodeProgress: 0 },
|
|
|
- });
|
|
|
- res.json({ message: `Reset ${stuck.length} stuck job(s) to PENDING`, count: stuck.length });
|
|
|
} catch (err) {
|
|
|
console.error('Reprocess-all error:', err);
|
|
|
res.status(500).json({ error: 'Internal server error' });
|