Full rental marketplace with 6 categories (apartment, house, car, motorcycle, bicycle, ebike). Booking workflow: create → confirm → pay → active → complete → payout. Landlord dashboard, admin moderation, availability calendar, Stripe Connect payouts. 14 QA bugs found and fixed including validator schemas, API response types, HTTP methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
import { Router } from 'express';
|
|
import { prisma } from '../../config/database.js';
|
|
import { requireModerator } from '../../middleware/requireRole.js';
|
|
import { validate } from '../../middleware/validate.js';
|
|
import { resolveReportSchema } from '../../validators/admin.js';
|
|
import { AppError } from '../../middleware/errorHandler.js';
|
|
|
|
const router = Router();
|
|
|
|
// GET /api/admin/reports
|
|
router.get('/', requireModerator, async (req, res, next) => {
|
|
try {
|
|
const { page = '1', pageSize = '20', status, targetType } = req.query;
|
|
const skip = (parseInt(page as string) - 1) * parseInt(pageSize as string);
|
|
const take = parseInt(pageSize as string);
|
|
|
|
const where: any = {};
|
|
if (status) where.status = status;
|
|
if (targetType) where.targetType = targetType;
|
|
|
|
const [reports, total] = await Promise.all([
|
|
prisma.report.findMany({
|
|
where,
|
|
include: {
|
|
reporter: { select: { id: true, fullName: true, avatar: true } },
|
|
},
|
|
skip,
|
|
take,
|
|
orderBy: { createdAt: 'desc' },
|
|
}),
|
|
prisma.report.count({ where }),
|
|
]);
|
|
|
|
res.json({
|
|
data: reports,
|
|
total,
|
|
page: parseInt(page as string),
|
|
pageSize: take,
|
|
totalPages: Math.ceil(total / take),
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// GET /api/admin/reports/:id
|
|
router.get('/:id', requireModerator, async (req, res, next) => {
|
|
try {
|
|
const report = await prisma.report.findUnique({
|
|
where: { id: req.params.id },
|
|
include: {
|
|
reporter: { select: { id: true, fullName: true, avatar: true, email: true } },
|
|
},
|
|
});
|
|
if (!report) throw new AppError(404, 'Report not found');
|
|
|
|
let target: any = null;
|
|
if (report.targetType === 'LISTING') {
|
|
target = await prisma.listing.findUnique({
|
|
where: { id: report.targetId },
|
|
select: { id: true, title: true, status: true, seller: { select: { id: true, fullName: true } } },
|
|
});
|
|
} else {
|
|
target = await prisma.user.findUnique({
|
|
where: { id: report.targetId },
|
|
select: { id: true, fullName: true, email: true, isBanned: true },
|
|
});
|
|
}
|
|
|
|
res.json({ report, target });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// PATCH /api/admin/reports/:id
|
|
router.patch('/:id', requireModerator, validate(resolveReportSchema), async (req, res, next) => {
|
|
try {
|
|
const report = await prisma.report.findUnique({ where: { id: req.params.id } });
|
|
if (!report) throw new AppError(404, 'Report not found');
|
|
|
|
const updated = await prisma.report.update({
|
|
where: { id: req.params.id },
|
|
data: {
|
|
status: req.body.status,
|
|
resolution: req.body.resolution,
|
|
resolvedBy: req.userId,
|
|
},
|
|
});
|
|
|
|
await prisma.notification.create({
|
|
data: {
|
|
userId: report.reporterId,
|
|
type: 'REPORT_RESOLVED',
|
|
title: 'Report Updated',
|
|
body: `Your report has been ${req.body.status.toLowerCase()}.`,
|
|
data: { reportId: report.id },
|
|
},
|
|
});
|
|
|
|
res.json(updated);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default router;
|