feat : Init Project
This commit is contained in:
230
nest-backend/src/routes/bugs.ts
Normal file
230
nest-backend/src/routes/bugs.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import { authenticate, requireStaff, requireAdmin } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
let bugCounter = 0;
|
||||
|
||||
async function nextUniqueCode(): Promise<string> {
|
||||
if (bugCounter === 0) {
|
||||
const last = await prisma.bugReport.findFirst({ orderBy: { uniqueCode: 'desc' } });
|
||||
if (last) {
|
||||
bugCounter = parseInt(last.uniqueCode.replace('HH-', ''), 10);
|
||||
}
|
||||
}
|
||||
bugCounter++;
|
||||
return `HH-${String(bugCounter).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
// Inline include to allow Prisma to correctly infer the return type
|
||||
function includeRelations() {
|
||||
return {
|
||||
submittedBy: { select: { id: true, username: true } },
|
||||
assignedTo: { select: { id: true, username: true } },
|
||||
comments: {
|
||||
include: { author: { select: { id: true, username: true } } },
|
||||
orderBy: { createdAt: 'asc' as const },
|
||||
},
|
||||
notes: {
|
||||
include: { author: { select: { id: true, username: true } } },
|
||||
orderBy: { createdAt: 'asc' as const },
|
||||
},
|
||||
meTooBugs: { select: { userId: true } },
|
||||
} as const;
|
||||
}
|
||||
|
||||
type BugWithRelations = Awaited<ReturnType<typeof getBugWithRelations>>;
|
||||
async function getBugWithRelations(id: string) {
|
||||
return prisma.bugReport.findUnique({ where: { id }, include: includeRelations() });
|
||||
}
|
||||
|
||||
function formatBug(bug: NonNullable<BugWithRelations>) {
|
||||
return {
|
||||
id: bug.id,
|
||||
uniqueCode: bug.uniqueCode,
|
||||
title: bug.title,
|
||||
description: bug.description,
|
||||
stepsToReproduce: bug.stepsToReproduce,
|
||||
severity: bug.severity,
|
||||
gameVersion: bug.gameVersion,
|
||||
screenshotUrl: bug.screenshotUrl,
|
||||
status: bug.status,
|
||||
submittedById: bug.submittedBy.id,
|
||||
submittedByName: bug.submittedBy.username,
|
||||
assignedToId: bug.assignedTo?.id ?? null,
|
||||
assignedToName: bug.assignedTo?.username ?? null,
|
||||
createdAt: bug.createdAt.toISOString(),
|
||||
updatedAt: bug.updatedAt.toISOString(),
|
||||
meTooBugs: bug.meTooBugs.map((m) => m.userId),
|
||||
comments: bug.comments.map((c) => ({
|
||||
id: c.id,
|
||||
bugReportId: bug.id,
|
||||
authorId: c.author.id,
|
||||
authorName: c.author.username,
|
||||
content: c.content,
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
})),
|
||||
notes: bug.notes.map((n) => ({
|
||||
id: n.id,
|
||||
bugReportId: bug.id,
|
||||
authorId: n.author.id,
|
||||
authorName: n.author.username,
|
||||
content: n.content,
|
||||
createdAt: n.createdAt.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// GET /api/bugs?status=...&severity=...&assignedTo=...&page=1&limit=20
|
||||
router.get('/', async (req: Request, res: Response): Promise<void> => {
|
||||
const page = Math.max(1, parseInt(req.query.page as string) || 1);
|
||||
const limit = Math.min(50, parseInt(req.query.limit as string) || 20);
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
if (req.query.status && req.query.status !== 'all') where.status = req.query.status;
|
||||
if (req.query.severity && req.query.severity !== 'all') where.severity = req.query.severity;
|
||||
if (req.query.assignedTo && req.query.assignedTo !== 'all') {
|
||||
where.assignedToId = req.query.assignedTo === 'unassigned' ? null : req.query.assignedTo;
|
||||
}
|
||||
|
||||
const [bugs, total] = await Promise.all([
|
||||
prisma.bugReport.findMany({
|
||||
where,
|
||||
include: includeRelations(),
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.bugReport.count({ where }),
|
||||
]);
|
||||
|
||||
res.json({ bugs: bugs.map(formatBug), total, page, pages: Math.ceil(total / limit) });
|
||||
});
|
||||
|
||||
// GET /api/bugs/:id
|
||||
router.get('/:id', async (req: Request, res: Response): Promise<void> => {
|
||||
const bug = await getBugWithRelations(req.params.id);
|
||||
if (!bug) { res.status(404).json({ error: 'Bug report not found' }); return; }
|
||||
res.json(formatBug(bug));
|
||||
});
|
||||
|
||||
// POST /api/bugs (authenticated)
|
||||
router.post('/', authenticate, async (req: Request, res: Response): Promise<void> => {
|
||||
const schema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
description: z.string().min(1),
|
||||
stepsToReproduce: z.string().min(1),
|
||||
severity: z.enum(['low', 'medium', 'high', 'critical']),
|
||||
gameVersion: z.string().min(1),
|
||||
screenshotUrl: z.string().url().optional(),
|
||||
});
|
||||
const parsed = schema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ error: parsed.error.flatten() }); return; }
|
||||
|
||||
const uniqueCode = await nextUniqueCode();
|
||||
|
||||
const bug = await prisma.bugReport.create({
|
||||
data: { ...parsed.data, uniqueCode, submittedById: req.user!.userId },
|
||||
include: includeRelations(),
|
||||
});
|
||||
|
||||
res.status(201).json(formatBug(bug));
|
||||
});
|
||||
|
||||
// PATCH /api/bugs/:id (staff or admin)
|
||||
router.patch('/:id', authenticate, requireStaff, async (req: Request, res: Response): Promise<void> => {
|
||||
const schema = z.object({
|
||||
status: z.enum(['open', 'in_progress', 'resolved', 'closed']).optional(),
|
||||
assignedToId: z.string().nullable().optional(),
|
||||
severity: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
});
|
||||
const parsed = schema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ error: parsed.error.flatten() }); return; }
|
||||
|
||||
const bug = await prisma.bugReport.update({
|
||||
where: { id: req.params.id },
|
||||
data: parsed.data,
|
||||
include: includeRelations(),
|
||||
});
|
||||
|
||||
res.json(formatBug(bug));
|
||||
});
|
||||
|
||||
// POST /api/bugs/:id/me-too (authenticated — toggle)
|
||||
router.post('/:id/me-too', authenticate, async (req: Request, res: Response): Promise<void> => {
|
||||
const userId = req.user!.userId;
|
||||
const bugReportId = req.params.id;
|
||||
|
||||
const existing = await prisma.meTooBug.findUnique({
|
||||
where: { userId_bugReportId: { userId, bugReportId } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await prisma.meTooBug.delete({ where: { userId_bugReportId: { userId, bugReportId } } });
|
||||
} else {
|
||||
await prisma.meTooBug.create({ data: { userId, bugReportId } });
|
||||
}
|
||||
|
||||
const bug = await getBugWithRelations(bugReportId);
|
||||
if (!bug) { res.status(404).json({ error: 'Bug report not found' }); return; }
|
||||
res.json(formatBug(bug));
|
||||
});
|
||||
|
||||
// POST /api/bugs/:id/comments (authenticated)
|
||||
router.post('/:id/comments', authenticate, async (req: Request, res: Response): Promise<void> => {
|
||||
const schema = z.object({ content: z.string().min(1) });
|
||||
const parsed = schema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ error: parsed.error.flatten() }); return; }
|
||||
|
||||
const comment = await prisma.bugComment.create({
|
||||
data: { content: parsed.data.content, bugReportId: req.params.id, authorId: req.user!.userId },
|
||||
include: { author: { select: { id: true, username: true } } },
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: comment.id,
|
||||
bugReportId: req.params.id,
|
||||
authorId: comment.author.id,
|
||||
authorName: comment.author.username,
|
||||
content: comment.content,
|
||||
createdAt: comment.createdAt.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// DELETE /api/bugs/comments/:id (admin only)
|
||||
router.delete('/comments/:id', authenticate, requireAdmin, async (req: Request, res: Response): Promise<void> => {
|
||||
await prisma.bugComment.delete({ where: { id: req.params.id } });
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
// POST /api/bugs/:id/notes (staff only)
|
||||
router.post('/:id/notes', authenticate, requireStaff, async (req: Request, res: Response): Promise<void> => {
|
||||
const schema = z.object({ content: z.string().min(1) });
|
||||
const parsed = schema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ error: parsed.error.flatten() }); return; }
|
||||
|
||||
const note = await prisma.bugReportNote.create({
|
||||
data: { content: parsed.data.content, bugReportId: req.params.id, authorId: req.user!.userId },
|
||||
include: { author: { select: { id: true, username: true } } },
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: note.id,
|
||||
bugReportId: req.params.id,
|
||||
authorId: note.author.id,
|
||||
authorName: note.author.username,
|
||||
content: note.content,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// DELETE /api/bugs/notes/:id (admin only)
|
||||
router.delete('/notes/:id', authenticate, requireAdmin, async (req: Request, res: Response): Promise<void> => {
|
||||
await prisma.bugReportNote.delete({ where: { id: req.params.id } });
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user