feat: implement user reporting system with dedicated module, service, controller, DTOs, and Prisma schema updates.

This commit is contained in:
pradeepkumar
2026-03-19 05:19:47 +05:30
parent 4d5a3fc36d
commit 76b7365dc3
9 changed files with 283 additions and 0 deletions

View File

@@ -156,6 +156,10 @@ model User {
subscriptions AgentSubscription[]
payments Payment[]
// Reports
reportsSubmitted UserReport[] @relation("ReportsSubmitted")
reportsReceived UserReport[] @relation("ReportsReceived")
@@index([email])
@@index([role])
@@index([status])
@@ -702,3 +706,34 @@ model Payment {
@@index([status])
@@map("payments")
}
// ============================================
// USER REPORTS
// ============================================
enum ReportStatus {
PENDING
REVIEWED
RESOLVED
DISMISSED
}
model UserReport {
id String @id @default(uuid())
reporterId String
reportedUserId String
conversationId String?
reason String
description String?
status ReportStatus @default(PENDING)
adminNotes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reporter User @relation("ReportsSubmitted", fields: [reporterId], references: [id], onDelete: Cascade)
reportedUser User @relation("ReportsReceived", fields: [reportedUserId], references: [id], onDelete: Cascade)
@@index([reporterId])
@@index([reportedUserId])
@@index([status])
@@map("user_reports")
}

View File

@@ -20,6 +20,7 @@ import { CmsModule } from './cms';
import { NotificationsModule } from './notifications';
import { TestimonialsModule } from './testimonials';
import { SupportChatModule } from './support-chat';
import { UserReportsModule } from './user-reports';
import { StripeModule } from './stripe';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@@ -64,6 +65,7 @@ import { AppService } from './app.service';
NotificationsModule,
TestimonialsModule,
SupportChatModule,
UserReportsModule,
StripeModule,
],
controllers: [AppController],

View File

@@ -0,0 +1,24 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
export class CreateReportDto {
@ApiProperty({ description: 'ID of the user being reported' })
@IsUUID()
@IsNotEmpty()
reportedUserId: string;
@ApiPropertyOptional({ description: 'Conversation ID where the issue occurred' })
@IsUUID()
@IsOptional()
conversationId?: string;
@ApiProperty({ description: 'Reason for the report', example: 'Spam' })
@IsString()
@IsNotEmpty()
reason: string;
@ApiPropertyOptional({ description: 'Additional details about the report' })
@IsString()
@IsOptional()
description?: string;
}

View File

@@ -0,0 +1,2 @@
export { CreateReportDto } from './create-report.dto';
export { UpdateReportDto } from './update-report.dto';

View File

@@ -0,0 +1,14 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum } from 'class-validator';
import { ReportStatus } from '@prisma/client';
export class UpdateReportDto {
@ApiProperty({ enum: ReportStatus, description: 'New status for the report' })
@IsEnum(ReportStatus)
status: ReportStatus;
@ApiPropertyOptional({ description: 'Admin notes about the report' })
@IsString()
@IsOptional()
adminNotes?: string;
}

View File

@@ -0,0 +1,3 @@
export { UserReportsModule } from './user-reports.module';
export { UserReportsService } from './user-reports.service';
export { UserReportsController } from './user-reports.controller';

View File

@@ -0,0 +1,87 @@
import {
Controller,
Get,
Post,
Patch,
Body,
Param,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger';
import { UserReportsService } from './user-reports.service';
import { CreateReportDto, UpdateReportDto } from './dto';
import { JwtAuthGuard } from '../auth/guards';
import { CurrentUser } from '../auth/decorators';
import { Roles } from '../auth/decorators/roles.decorator';
import { UserRole, ReportStatus } from '@prisma/client';
@ApiTags('User Reports')
@Controller('user-reports')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
export class UserReportsController {
constructor(private readonly userReportsService: UserReportsService) {}
// ==========================================
// USER: SUBMIT A REPORT
// ==========================================
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Submit a report against a user' })
async createReport(
@CurrentUser('id') reporterId: string,
@Body() dto: CreateReportDto,
) {
return this.userReportsService.createReport(reporterId, dto);
}
// ==========================================
// ADMIN: GET ALL REPORTS
// ==========================================
@Get('admin/all')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Admin: Get all user reports' })
@ApiQuery({ name: 'status', required: false, enum: ReportStatus })
async getAllReports(@Query('status') status?: ReportStatus) {
return this.userReportsService.getAllReports(status);
}
// ==========================================
// ADMIN: GET REPORT COUNTS
// ==========================================
@Get('admin/counts')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Admin: Get report counts by status' })
async getReportCounts() {
return this.userReportsService.getReportCounts();
}
// ==========================================
// ADMIN: GET SINGLE REPORT
// ==========================================
@Get('admin/:id')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Admin: Get a specific report' })
@ApiParam({ name: 'id', description: 'Report ID' })
async getReport(@Param('id') id: string) {
return this.userReportsService.getReportById(id);
}
// ==========================================
// ADMIN: UPDATE REPORT STATUS
// ==========================================
@Patch('admin/:id')
@Roles(UserRole.ADMIN)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Admin: Update report status and notes' })
@ApiParam({ name: 'id', description: 'Report ID' })
async updateReport(
@Param('id') id: string,
@Body() dto: UpdateReportDto,
) {
return this.userReportsService.updateReport(id, dto.status, dto.adminNotes);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UserReportsController } from './user-reports.controller';
import { UserReportsService } from './user-reports.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [UserReportsController],
providers: [UserReportsService],
exports: [UserReportsService],
})
export class UserReportsModule {}

View File

@@ -0,0 +1,104 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ReportStatus } from '@prisma/client';
import { CreateReportDto } from './dto';
@Injectable()
export class UserReportsService {
constructor(private readonly prisma: PrismaService) {}
private readonly reportInclude = {
reporter: {
select: {
id: true,
email: true,
userProfile: { select: { firstName: true, lastName: true } },
agentProfile: { select: { firstName: true, lastName: true } },
},
},
reportedUser: {
select: {
id: true,
email: true,
userProfile: { select: { firstName: true, lastName: true } },
agentProfile: { select: { firstName: true, lastName: true } },
},
},
};
async createReport(reporterId: string, dto: CreateReportDto) {
if (reporterId === dto.reportedUserId) {
throw new BadRequestException('You cannot report yourself');
}
// Check if user already reported this user recently (within 24h)
const recentReport = await this.prisma.userReport.findFirst({
where: {
reporterId,
reportedUserId: dto.reportedUserId,
createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
});
if (recentReport) {
throw new BadRequestException('You have already reported this user recently');
}
return this.prisma.userReport.create({
data: {
reporterId,
reportedUserId: dto.reportedUserId,
conversationId: dto.conversationId,
reason: dto.reason,
description: dto.description,
},
include: this.reportInclude,
});
}
async getAllReports(status?: ReportStatus) {
const where = status ? { status } : {};
return this.prisma.userReport.findMany({
where,
include: this.reportInclude,
orderBy: { createdAt: 'desc' },
});
}
async getReportById(id: string) {
const report = await this.prisma.userReport.findUnique({
where: { id },
include: this.reportInclude,
});
if (!report) {
throw new NotFoundException('Report not found');
}
return report;
}
async updateReport(id: string, status: ReportStatus, adminNotes?: string) {
const report = await this.prisma.userReport.findUnique({ where: { id } });
if (!report) {
throw new NotFoundException('Report not found');
}
return this.prisma.userReport.update({
where: { id },
data: { status, adminNotes },
include: this.reportInclude,
});
}
async getReportCounts() {
const [pending, reviewed, resolved, dismissed, total] = await Promise.all([
this.prisma.userReport.count({ where: { status: ReportStatus.PENDING } }),
this.prisma.userReport.count({ where: { status: ReportStatus.REVIEWED } }),
this.prisma.userReport.count({ where: { status: ReportStatus.RESOLVED } }),
this.prisma.userReport.count({ where: { status: ReportStatus.DISMISSED } }),
this.prisma.userReport.count(),
]);
return { pending, reviewed, resolved, dismissed, total };
}
}