Implement report verification interface with audit log
- Add verify/dispute endpoints to backend API - Create verification service methods with auto-hide logic for 3+ disputes - Update ReportDetailModal with verification buttons and comment forms - Add verification audit log display showing history of verifications/disputes - Implement auto-hide warning for disputed reports - Update MapPage to handle verification actions and refresh data - Add verification counts to report responses - Filter out disputed reports from public view by default - Add verification test script Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
This commit is contained in:
@@ -277,6 +277,114 @@ export class ReportController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async verifyReport(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
if (!req.userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User not authenticated',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { comment } = req.body;
|
||||
|
||||
// Find or create user in database
|
||||
const dbUserId = await this.findOrCreateUser(req.userId);
|
||||
|
||||
const report = await reportService.verifyReport(id, dbUserId, comment);
|
||||
|
||||
res.json(report);
|
||||
} catch (error: any) {
|
||||
console.error('Error verifying report:', error);
|
||||
|
||||
if (error.message === 'Report not found') {
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
code: 'RESOURCE_NOT_FOUND',
|
||||
message: 'Report not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message === 'You have already verified or disputed this report') {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'You have already verified or disputed this report',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to verify report',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async disputeReport(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
if (!req.userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User not authenticated',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { comment } = req.body;
|
||||
|
||||
// Find or create user in database
|
||||
const dbUserId = await this.findOrCreateUser(req.userId);
|
||||
|
||||
const report = await reportService.disputeReport(id, dbUserId, comment);
|
||||
|
||||
res.json(report);
|
||||
} catch (error: any) {
|
||||
console.error('Error disputing report:', error);
|
||||
|
||||
if (error.message === 'Report not found') {
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
code: 'RESOURCE_NOT_FOUND',
|
||||
message: 'Report not found',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message === 'You have already verified or disputed this report') {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'You have already verified or disputed this report',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to dispute report',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const reportController = new ReportController();
|
||||
|
||||
@@ -14,4 +14,8 @@ router.get('/', reportController.getReports.bind(reportController));
|
||||
router.get('/:id', reportController.getReportById.bind(reportController));
|
||||
router.delete('/:id', requireAuth, reportController.deleteReport.bind(reportController));
|
||||
|
||||
// Verification endpoints
|
||||
router.post('/:id/verify', requireAuth, reportController.verifyReport.bind(reportController));
|
||||
router.post('/:id/dispute', requireAuth, reportController.disputeReport.bind(reportController));
|
||||
|
||||
export default router;
|
||||
|
||||
75
server/src/scripts/testVerification.ts
Normal file
75
server/src/scripts/testVerification.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import prisma from '../config/database';
|
||||
|
||||
async function testVerification() {
|
||||
try {
|
||||
console.log('Testing verification system...\n');
|
||||
|
||||
// Get a test report
|
||||
const report = await prisma.report.findFirst({
|
||||
include: {
|
||||
verifications: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
console.log('No reports found in database. Please create a report first.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Test Report:');
|
||||
console.log(` ID: ${report.id}`);
|
||||
console.log(` Litter Type: ${report.litterType}`);
|
||||
console.log(` Status: ${report.verificationStatus}`);
|
||||
console.log(` Location: ${report.latitude}, ${report.longitude}`);
|
||||
|
||||
// Count verifications
|
||||
const verifyCount = report.verifications.filter(v => v.verificationType === 'verify').length;
|
||||
const disputeCount = report.verifications.filter(v => v.verificationType === 'dispute').length;
|
||||
|
||||
console.log(`\nVerification Stats:`);
|
||||
console.log(` Verifications: ${verifyCount}`);
|
||||
console.log(` Disputes: ${disputeCount}`);
|
||||
|
||||
if (report.verifications.length > 0) {
|
||||
console.log(`\nVerification History:`);
|
||||
report.verifications.forEach((v, i) => {
|
||||
console.log(` ${i + 1}. ${v.user.username} - ${v.verificationType} ${v.comment ? `("${v.comment}")` : ''}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Check auto-hide logic
|
||||
if (disputeCount >= 3) {
|
||||
console.log(`\n⚠️ This report should be auto-hidden (3+ disputes)`);
|
||||
console.log(` Current status: ${report.verificationStatus}`);
|
||||
if (report.verificationStatus !== 'disputed') {
|
||||
console.log(` ❌ ERROR: Status should be "disputed" but is "${report.verificationStatus}"`);
|
||||
} else {
|
||||
console.log(` ✅ Status correctly set to "disputed"`);
|
||||
}
|
||||
} else if (verifyCount >= 2) {
|
||||
console.log(`\n✅ This report should be verified (2+ verifications)`);
|
||||
console.log(` Current status: ${report.verificationStatus}`);
|
||||
if (report.verificationStatus !== 'verified') {
|
||||
console.log(` ⚠️ Status should be "verified" but is "${report.verificationStatus}"`);
|
||||
} else {
|
||||
console.log(` ✅ Status correctly set to "verified"`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Verification system test complete!');
|
||||
} catch (error) {
|
||||
console.error('Error testing verification:', error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
testVerification();
|
||||
@@ -79,6 +79,7 @@ export class ReportService {
|
||||
quantities?: string[];
|
||||
verificationStatus?: string[];
|
||||
bounds?: { north: number; south: number; east: number; west: number };
|
||||
includeDisputed?: boolean; // Admin flag to include disputed reports
|
||||
}) {
|
||||
const where: any = {};
|
||||
|
||||
@@ -98,6 +99,9 @@ export class ReportService {
|
||||
|
||||
if (filters?.verificationStatus?.length) {
|
||||
where.verificationStatus = { in: filters.verificationStatus };
|
||||
} else if (!filters?.includeDisputed) {
|
||||
// By default, exclude disputed reports (auto-hidden)
|
||||
where.verificationStatus = { not: 'disputed' };
|
||||
}
|
||||
|
||||
if (filters?.bounds) {
|
||||
@@ -122,14 +126,33 @@ export class ReportService {
|
||||
},
|
||||
},
|
||||
environmentalConcerns: true,
|
||||
verifications: true,
|
||||
verifications: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return reports;
|
||||
// Add verification and dispute counts to each report
|
||||
return reports.map(report => {
|
||||
const verificationCount = report.verifications.filter(v => v.verificationType === 'verify').length;
|
||||
const disputeCount = report.verifications.filter(v => v.verificationType === 'dispute').length;
|
||||
|
||||
return {
|
||||
...report,
|
||||
verificationCount,
|
||||
disputeCount,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getReportById(id: string) {
|
||||
@@ -153,11 +176,26 @@ export class ReportService {
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate verification and dispute counts
|
||||
const verificationCount = report.verifications.filter(v => v.verificationType === 'verify').length;
|
||||
const disputeCount = report.verifications.filter(v => v.verificationType === 'dispute').length;
|
||||
|
||||
return {
|
||||
...report,
|
||||
verificationCount,
|
||||
disputeCount,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteReport(id: string, userId: string) {
|
||||
@@ -179,6 +217,115 @@ export class ReportService {
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async verifyReport(reportId: string, userId: string, comment?: string) {
|
||||
// Check if report exists
|
||||
const report = await prisma.report.findUnique({
|
||||
where: { id: reportId },
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
throw new Error('Report not found');
|
||||
}
|
||||
|
||||
// Check if user already verified/disputed this report
|
||||
const existingVerification = await prisma.verification.findUnique({
|
||||
where: {
|
||||
reportId_userId: {
|
||||
reportId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingVerification) {
|
||||
throw new Error('You have already verified or disputed this report');
|
||||
}
|
||||
|
||||
// Create verification
|
||||
await prisma.verification.create({
|
||||
data: {
|
||||
reportId,
|
||||
userId,
|
||||
verificationType: 'verify',
|
||||
comment,
|
||||
},
|
||||
});
|
||||
|
||||
// Update verification counts and status
|
||||
await this.updateReportVerificationStatus(reportId);
|
||||
|
||||
// Return updated report
|
||||
return this.getReportById(reportId);
|
||||
}
|
||||
|
||||
async disputeReport(reportId: string, userId: string, comment?: string) {
|
||||
// Check if report exists
|
||||
const report = await prisma.report.findUnique({
|
||||
where: { id: reportId },
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
throw new Error('Report not found');
|
||||
}
|
||||
|
||||
// Check if user already verified/disputed this report
|
||||
const existingVerification = await prisma.verification.findUnique({
|
||||
where: {
|
||||
reportId_userId: {
|
||||
reportId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingVerification) {
|
||||
throw new Error('You have already verified or disputed this report');
|
||||
}
|
||||
|
||||
// Create dispute
|
||||
await prisma.verification.create({
|
||||
data: {
|
||||
reportId,
|
||||
userId,
|
||||
verificationType: 'dispute',
|
||||
comment,
|
||||
},
|
||||
});
|
||||
|
||||
// Update verification counts and status
|
||||
await this.updateReportVerificationStatus(reportId);
|
||||
|
||||
// Return updated report
|
||||
return this.getReportById(reportId);
|
||||
}
|
||||
|
||||
private async updateReportVerificationStatus(reportId: string) {
|
||||
// Count verifications and disputes
|
||||
const verifications = await prisma.verification.findMany({
|
||||
where: { reportId },
|
||||
});
|
||||
|
||||
const verifyCount = verifications.filter(v => v.verificationType === 'verify').length;
|
||||
const disputeCount = verifications.filter(v => v.verificationType === 'dispute').length;
|
||||
|
||||
// Determine status based on counts
|
||||
let status: 'pending' | 'verified' | 'disputed' = 'pending';
|
||||
|
||||
if (disputeCount >= 3) {
|
||||
// Auto-hide: 3+ disputes
|
||||
status = 'disputed';
|
||||
} else if (verifyCount >= 2) {
|
||||
// Verified: 2+ verifications
|
||||
status = 'verified';
|
||||
}
|
||||
|
||||
// Update report status
|
||||
await prisma.report.update({
|
||||
where: { id: reportId },
|
||||
data: { verificationStatus: status },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const reportService = new ReportService();
|
||||
|
||||
Reference in New Issue
Block a user