feat: implement user reporting system with dedicated module, service, controller, DTOs, and Prisma schema updates.
This commit is contained in:
@@ -156,6 +156,10 @@ model User {
|
|||||||
subscriptions AgentSubscription[]
|
subscriptions AgentSubscription[]
|
||||||
payments Payment[]
|
payments Payment[]
|
||||||
|
|
||||||
|
// Reports
|
||||||
|
reportsSubmitted UserReport[] @relation("ReportsSubmitted")
|
||||||
|
reportsReceived UserReport[] @relation("ReportsReceived")
|
||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@@index([role])
|
@@index([role])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
@@ -702,3 +706,34 @@ model Payment {
|
|||||||
@@index([status])
|
@@index([status])
|
||||||
@@map("payments")
|
@@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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { CmsModule } from './cms';
|
|||||||
import { NotificationsModule } from './notifications';
|
import { NotificationsModule } from './notifications';
|
||||||
import { TestimonialsModule } from './testimonials';
|
import { TestimonialsModule } from './testimonials';
|
||||||
import { SupportChatModule } from './support-chat';
|
import { SupportChatModule } from './support-chat';
|
||||||
|
import { UserReportsModule } from './user-reports';
|
||||||
import { StripeModule } from './stripe';
|
import { StripeModule } from './stripe';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
@@ -64,6 +65,7 @@ import { AppService } from './app.service';
|
|||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
TestimonialsModule,
|
TestimonialsModule,
|
||||||
SupportChatModule,
|
SupportChatModule,
|
||||||
|
UserReportsModule,
|
||||||
StripeModule,
|
StripeModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
24
src/user-reports/dto/create-report.dto.ts
Normal file
24
src/user-reports/dto/create-report.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
2
src/user-reports/dto/index.ts
Normal file
2
src/user-reports/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { CreateReportDto } from './create-report.dto';
|
||||||
|
export { UpdateReportDto } from './update-report.dto';
|
||||||
14
src/user-reports/dto/update-report.dto.ts
Normal file
14
src/user-reports/dto/update-report.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
3
src/user-reports/index.ts
Normal file
3
src/user-reports/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { UserReportsModule } from './user-reports.module';
|
||||||
|
export { UserReportsService } from './user-reports.service';
|
||||||
|
export { UserReportsController } from './user-reports.controller';
|
||||||
87
src/user-reports/user-reports.controller.ts
Normal file
87
src/user-reports/user-reports.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/user-reports/user-reports.module.ts
Normal file
12
src/user-reports/user-reports.module.ts
Normal 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 {}
|
||||||
104
src/user-reports/user-reports.service.ts
Normal file
104
src/user-reports/user-reports.service.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user