-
Notifications
You must be signed in to change notification settings - Fork 63
Feat/secure #341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/secure #341
Changes from all commits
b0dda70
dd64634
8047fe0
56b5d44
994662e
771c207
2f827c8
228c2ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,24 @@ | ||
| import { Controller, Post, Get, Put, Body, Param, Query, UseGuards } from '@nestjs/common'; | ||
| import { Controller, Post, Get, Put, Body, Param, Query, UseGuards, Req } from '@nestjs/common'; | ||
| import { ComplianceService } from './compliance.service'; | ||
| import { ExpenseCategory } from './entities/expense-category.entity'; | ||
| import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; | ||
| import { AuthorizationGuard } from '../auth/guards/authorization.guard'; | ||
| import { RequirePermissions } from '../auth/decorators/permissions.decorator'; | ||
| import { Permissions } from '../auth/decorators/permissions.decorator'; | ||
|
|
||
| interface AuthRequest { | ||
| user: { walletAddress: string }; | ||
| } | ||
|
|
||
| @Controller('api/compliance') | ||
| @UseGuards(JwtAuthGuard, AuthorizationGuard) | ||
| export class ComplianceController { | ||
| constructor(private readonly complianceService: ComplianceService) { } | ||
|
|
||
| @Post('export/request') | ||
| async requestExport(@Body() data: any) { | ||
| // In a real app, userId would come from auth guard | ||
| const userId = data.userId; | ||
| return this.complianceService.requestExport(userId, data); | ||
| @RequirePermissions(Permissions.CAN_CREATE_EXPORT) | ||
| async requestExport(@Body() data: any, @Req() req: AuthRequest) { | ||
| return this.complianceService.requestExport(req.user.walletAddress, data); | ||
| } | ||
|
|
||
| @Get('export/:requestId/status') | ||
|
|
@@ -19,28 +27,32 @@ export class ComplianceController { | |
| } | ||
|
|
||
| @Get('categories') | ||
| async getCategories(@Query('userId') userId: string) { | ||
| return this.complianceService.getCategories(userId); | ||
| @RequirePermissions(Permissions.CAN_READ_EXPORT) | ||
| async getCategories(@Req() req: AuthRequest) { | ||
| return this.complianceService.getCategories(req.user.walletAddress); | ||
| } | ||
|
|
||
| @Post('categories') | ||
| async createCategory(@Body() data: any) { | ||
| const userId = data.userId; | ||
| return this.complianceService.createCategory(userId, data); | ||
| @RequirePermissions(Permissions.CAN_CREATE_EXPORT) | ||
| async createCategory(@Body() data: any, @Req() req: AuthRequest) { | ||
| return this.complianceService.createCategory(req.user.walletAddress, data); | ||
| } | ||
|
|
||
| @Put('splits/:splitId/category') | ||
| @RequirePermissions(Permissions.CAN_UPDATE_SPLIT) | ||
| async assignCategory(@Param('splitId') splitId: string, @Body('categoryId') categoryId: string) { | ||
| return this.complianceService.assignCategoryToSplit(splitId, categoryId); | ||
| } | ||
|
|
||
| @Get('summary') | ||
| async getSummary(@Query('userId') userId: string, @Query('year') year: string) { | ||
| return this.complianceService.getSummary(userId, parseInt(year)); | ||
| @RequirePermissions(Permissions.CAN_READ_EXPORT) | ||
| async getSummary(@Query('year') year: string, @Req() req: AuthRequest) { | ||
| return this.complianceService.getSummary(req.user.walletAddress, parseInt(year)); | ||
| } | ||
|
|
||
| @Get('tax-deductible-total') | ||
| async getTaxDeductibleTotal(@Query('userId') userId: string, @Query('period') period: string) { | ||
| return this.complianceService.getTaxDeductibleTotal(userId, period); | ||
| @Get('export/:requestId/download') | ||
| @RequirePermissions(Permissions.CAN_READ_EXPORT) | ||
| async downloadExport(@Param('requestId') requestId: string, @Req() req: AuthRequest) { | ||
| return this.complianceService.downloadExport(requestId, req.user.walletAddress); | ||
| } | ||
|
Comment on lines
+53
to
57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. File download response not properly handled for HTTP streaming. The 🔧 Proposed fix using StreamableFile-import { Controller, Post, Get, Put, Body, Param, Query, UseGuards, Req } from '@nestjs/common';
+import { Controller, Post, Get, Put, Body, Param, Query, UseGuards, Req, StreamableFile, Res, Header } from '@nestjs/common';
+import { Response } from 'express';
// ...
`@Get`('export/:requestId/download')
`@RequirePermissions`(Permissions.CAN_READ_EXPORT)
- async downloadExport(`@Param`('requestId') requestId: string, `@Req`() req: AuthRequest) {
- return this.complianceService.downloadExport(requestId, req.user.walletAddress);
+ async downloadExport(
+ `@Param`('requestId') requestId: string,
+ `@Req`() req: AuthRequest,
+ `@Res`({ passthrough: true }) res: Response,
+ ): Promise<StreamableFile> {
+ const { fileName, content, mimeType } = await this.complianceService.downloadExport(requestId, req.user.walletAddress);
+ res.set({
+ 'Content-Type': mimeType,
+ 'Content-Disposition': `attachment; filename="${fileName}"`,
+ });
+ return new StreamableFile(content);
}🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ import { PDFExporterService } from "./exporters/pdf-exporter.service"; | |
| import { QBOExporterService } from "./exporters/qbo-exporter.service"; | ||
| import { JSONExporterService } from "./exporters/json-exporter.service"; | ||
| import { OFXExporterService } from "./exporters/ofx-exporter.service"; | ||
| import { EmailService } from "../email/email.service"; | ||
| import { ProfileService } from "../profile/profile.service"; | ||
| import { Logger } from "@nestjs/common"; | ||
| import * as fs from "fs"; | ||
| import * as path from "path"; | ||
|
|
@@ -34,6 +34,7 @@ export class ComplianceProcessor { | |
| private jsonExporter: JSONExporterService, | ||
| private ofxExporter: OFXExporterService, | ||
| private emailService: EmailService, | ||
| private profileService: ProfileService, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing
🐛 Proposed fix: Import ProfileModule in ComplianceModuleIn import { EmailModule } from '../email/email.module';
+import { ProfileModule } from '../profile/profile.module';
`@Module`({
imports: [
TypeOrmModule.forFeature([ExpenseCategory, TaxExportRequest, Split]),
BullModule.registerQueue({
name: 'compliance-export',
}),
EmailModule,
+ ProfileModule,
],🤖 Prompt for AI Agents |
||
| ) { | ||
| if (!fs.existsSync(this.exportDir)) { | ||
| fs.mkdirSync(this.exportDir); | ||
|
|
@@ -99,25 +100,39 @@ export class ComplianceProcessor { | |
|
|
||
| await this.exportRepo.update(requestId, { | ||
| status: ExportStatus.READY, | ||
| fileUrl: filePath, // Using local path for simplicity in this implementation | ||
| fileUrl: `http://localhost:3000/api/compliance/export/${requestId}/download`, // Secure download URL | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded The 🔧 Proposed fix+// In constructor, inject ConfigService or use process.env
+private readonly baseUrl = process.env.APP_BASE_URL || 'http://localhost:3000';
// Then use it:
- fileUrl: `http://localhost:3000/api/compliance/export/${requestId}/download`,
+ fileUrl: `${this.baseUrl}/api/compliance/export/${requestId}/download`,🤖 Prompt for AI Agents |
||
| fileSize: fs.statSync(filePath).size, | ||
| recordCount: splits.length, | ||
| completedAt: new Date(), | ||
| expiresAt, | ||
| }); | ||
|
|
||
| // Send email notification | ||
| // In a real app, we'd look up the user's email by wallet address | ||
| // For now, we'll assume a dummy email or use a placeholder | ||
| await this.emailService["emailQueue"].add("sendEmail", { | ||
| to: "user@example.com", // Placeholder | ||
| type: "export_ready", | ||
| context: { | ||
| requestId, | ||
| format: request.exportFormat, | ||
| downloadUrl: `http://localhost:3000/api/compliance/export/${requestId}/download`, | ||
| }, | ||
| }); | ||
| try { | ||
| const profile = await this.profileService.getByWalletAddress(request.userId); | ||
| const userEmail = profile.email || 'user@example.com'; // fallback | ||
| await this.emailService["emailQueue"].add("sendEmail", { | ||
| to: userEmail, | ||
| type: "export_ready", | ||
| context: { | ||
| requestId, | ||
| format: request.exportFormat, | ||
| downloadUrl: `http://localhost:3000/api/compliance/export/${requestId}/download`, | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| this.logger.error(`Failed to get user email for export ${requestId}:`, error); | ||
| // Fallback to placeholder | ||
| await this.emailService["emailQueue"].add("sendEmail", { | ||
| to: "user@example.com", | ||
| type: "export_ready", | ||
| context: { | ||
| requestId, | ||
| format: request.exportFormat, | ||
| downloadUrl: `http://localhost:3000/api/compliance/export/${requestId}/download`, | ||
| }, | ||
| }); | ||
| } | ||
|
Comment on lines
+111
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sending emails to placeholder address The fallback to Consider:
🔧 Proposed fix try {
const profile = await this.profileService.getByWalletAddress(request.userId);
- const userEmail = profile.email || 'user@example.com'; // fallback
+ if (!profile.email) {
+ this.logger.warn(`No email found for user ${request.userId}, skipping export notification`);
+ return;
+ }
await this.emailService["emailQueue"].add("sendEmail", {
- to: userEmail,
+ to: profile.email,
type: "export_ready",
context: {
requestId,
format: request.exportFormat,
- downloadUrl: `http://localhost:3000/api/compliance/export/${requestId}/download`,
+ downloadUrl: `${this.baseUrl}/api/compliance/export/${requestId}/download`,
},
});
} catch (error) {
this.logger.error(`Failed to get user email for export ${requestId}:`, error);
- // Fallback to placeholder
- await this.emailService["emailQueue"].add("sendEmail", {
- to: "user@example.com",
- type: "export_ready",
- context: {
- requestId,
- format: request.exportFormat,
- downloadUrl: `http://localhost:3000/api/compliance/export/${requestId}/download`,
- },
- });
+ // Don't send to placeholder - log and skip
}🤖 Prompt for AI Agents |
||
|
|
||
| this.logger.log(`Export ${requestId} completed successfully`); | ||
| } catch (error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { Test, TestingModule } from '@nestjs/testing'; | ||
| import { getRepositoryToken } from '@nestjs/typeorm'; | ||
| import { Repository } from 'typeorm'; | ||
| import { ComplianceService } from './compliance.service'; | ||
| import { TaxExportRequest, ExportStatus } from './entities/tax-export-request.entity'; | ||
| import { ExpenseCategory } from './entities/expense-category.entity'; | ||
| import { Split } from '../entities/split.entity'; | ||
| import { BullModule } from '@nestjs/bull'; | ||
|
|
||
| describe('ComplianceService', () => { | ||
| let service: ComplianceService; | ||
| let exportRepo: Repository<TaxExportRequest>; | ||
| let categoryRepo: Repository<ExpenseCategory>; | ||
| let splitRepo: Repository<Split>; | ||
|
|
||
| beforeEach(async () => { | ||
| const module: TestingModule = await Test.createTestingModule({ | ||
| imports: [BullModule.registerQueue({ name: 'compliance-export' })], | ||
| providers: [ | ||
| ComplianceService, | ||
| { | ||
| provide: getRepositoryToken(TaxExportRequest), | ||
| useClass: Repository, | ||
| }, | ||
| { | ||
| provide: getRepositoryToken(ExpenseCategory), | ||
| useClass: Repository, | ||
| }, | ||
| { | ||
| provide: getRepositoryToken(Split), | ||
| useClass: Repository, | ||
| }, | ||
| ], | ||
| }).compile(); | ||
|
|
||
| service = module.get<ComplianceService>(ComplianceService); | ||
| exportRepo = module.get<Repository<TaxExportRequest>>(getRepositoryToken(TaxExportRequest)); | ||
| categoryRepo = module.get<Repository<ExpenseCategory>>(getRepositoryToken(ExpenseCategory)); | ||
| splitRepo = module.get<Repository<Split>>(getRepositoryToken(Split)); | ||
| }); | ||
|
|
||
| it('should be defined', () => { | ||
| expect(service).toBeDefined(); | ||
| }); | ||
|
|
||
| describe('requestExport', () => { | ||
| it('should create export request with QUEUED status', async () => { | ||
| const mockRequest = { | ||
| id: 'test-id', | ||
| userId: 'user-1', | ||
| exportFormat: 'CSV', | ||
| periodStart: new Date('2023-01-01'), | ||
| periodEnd: new Date('2023-12-31'), | ||
| status: ExportStatus.QUEUED, | ||
| }; | ||
| jest.spyOn(exportRepo, 'create').mockReturnValue(mockRequest as any); | ||
| jest.spyOn(exportRepo, 'save').mockResolvedValue(mockRequest as any); | ||
|
|
||
| const result = await service.requestExport('user-1', { | ||
| exportFormat: 'CSV', | ||
| periodStart: '2023-01-01', | ||
| periodEnd: '2023-12-31', | ||
| }); | ||
|
|
||
| expect(result.status).toBe(ExportStatus.QUEUED); | ||
| }); | ||
| }); | ||
|
|
||
| describe('getExportStatus', () => { | ||
| it('should return READY status', async () => { | ||
| const mockRequest = { id: 'test-id', status: ExportStatus.READY }; | ||
| jest.spyOn(exportRepo, 'findOne').mockResolvedValue(mockRequest as any); | ||
|
|
||
| const result = await service.getExportStatus('test-id'); | ||
| expect(result.status).toBe(ExportStatus.READY); | ||
| }); | ||
|
|
||
| it('should return FAILED status', async () => { | ||
| const mockRequest = { id: 'test-id', status: ExportStatus.FAILED }; | ||
| jest.spyOn(exportRepo, 'findOne').mockResolvedValue(mockRequest as any); | ||
|
|
||
| const result = await service.getExportStatus('test-id'); | ||
| expect(result.status).toBe(ExportStatus.FAILED); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -95,9 +95,34 @@ export class ComplianceService { | |||||||
| return summary; | ||||||||
| } | ||||||||
|
|
||||||||
| async getTaxDeductibleTotal(userId: string, period: string) { | ||||||||
| const year = parseInt(period); | ||||||||
| const summary = await this.getSummary(userId, year); | ||||||||
| async downloadExport(requestId: string, userId: string) { | ||||||||
| const request = await this.exportRepo.findOne({ where: { id: requestId, userId } }); | ||||||||
| if (!request) throw new NotFoundException('Export request not found or access denied'); | ||||||||
| if (request.status !== ExportStatus.READY) throw new BadRequestException('Export not ready'); | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 101 uses 🐛 Proposed fix-import { Injectable, NotFoundException } from '@nestjs/common';
+import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| // Return the file content or stream | ||||||||
| const fs = require('fs'); | ||||||||
| const path = require('path'); | ||||||||
| const filePath = path.join(process.cwd(), 'exports', `tax-export-${requestId}.${request.exportFormat.toLowerCase()}`); | ||||||||
| if (!fs.existsSync(filePath)) throw new NotFoundException('File not found'); | ||||||||
|
|
||||||||
| return { | ||||||||
| fileName: `tax-export-${requestId}.${request.exportFormat.toLowerCase()}`, | ||||||||
| content: fs.readFileSync(filePath), | ||||||||
| mimeType: this.getMimeType(request.exportFormat), | ||||||||
| }; | ||||||||
| } | ||||||||
|
|
||||||||
| private getMimeType(format: ExportFormat): string { | ||||||||
| switch (format) { | ||||||||
| case ExportFormat.CSV: return 'text/csv'; | ||||||||
| case ExportFormat.PDF: return 'application/pdf'; | ||||||||
| case ExportFormat.QBO: return 'application/octet-stream'; | ||||||||
| case ExportFormat.JSON: return 'application/json'; | ||||||||
| case ExportFormat.OFX: return 'application/xml'; | ||||||||
| default: return 'application/octet-stream'; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| return Object.values(summary).reduce((acc, curr) => acc + curr.deductible, 0); | ||||||||
| } | ||||||||
|
Comment on lines
127
to
128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Orphan code from removed method. Lines 127-128 appear to be leftover code from the removed 🐛 Proposed fix: Remove orphan lines default: return 'application/octet-stream';
}
}
-
- return Object.values(summary).reduce((acc, curr) => acc + curr.deductible, 0);
- }
}📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.4.9)[error] 127-127: expected a semicolon to end the class property, but found none (parse) [error] 127-127: expected a semicolon to end the class property, but found none (parse) [error] 127-127: Expected an identifier, a string literal, a number literal, a private field name, or a computed name but instead found '.'. (parse) [error] 127-127: Expected a class method body but instead found '.'. (parse) [error] 127-127: Expected a parameter but instead found '('. (parse) [error] 127-127: expected (parse) [error] 127-127: Expected a class method body but instead found '=>'. (parse) [error] 127-127: expected a semicolon to end the class property, but found none (parse) [error] 127-127: Expected an identifier, a string literal, a number literal, a private field name, or a computed name but instead found '+'. (parse) [error] 127-127: expected a semicolon to end the class property, but found none (parse) [error] 127-127: Expected an identifier, a string literal, a number literal, a private field name, or a computed name but instead found '.'. (parse) [error] 127-127: expected a semicolon to end the class property, but found none (parse) [error] 127-127: Expected an identifier, a string literal, a number literal, a private field name, or a computed name but instead found ','. (parse) 🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,10 @@ import { Repository } from "typeorm"; | |
|
|
||
| import { FraudDetectionService } from "./fraud-detection.service"; | ||
| import { FraudAlert, AlertStatus } from "./entities/fraud-alert.entity"; | ||
| import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; | ||
| import { AuthorizationGuard } from "../auth/guards/authorization.guard"; | ||
| import { RequirePermissions } from "../auth/decorators/permissions.decorator"; | ||
| import { Permissions } from "../auth/decorators/permissions.decorator"; | ||
| import { | ||
| AnalyzeSplitRequestDto, | ||
| AnalyzePaymentRequestDto, | ||
|
|
@@ -22,6 +26,7 @@ import { | |
| } from "./dto/analyze-split.dto"; | ||
|
|
||
| @Controller("fraud") | ||
| @UseGuards(JwtAuthGuard, AuthorizationGuard) | ||
| export class FraudDetectionController { | ||
| constructor( | ||
| private readonly fraudDetectionService: FraudDetectionService, | ||
|
|
@@ -33,6 +38,7 @@ export class FraudDetectionController { | |
| * Get all fraud alerts | ||
| */ | ||
| @Get("alerts") | ||
| @RequirePermissions(Permissions.CAN_READ_FRAUD_ALERTS) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify CAN_READ_FRAUD_ALERTS does not exist
echo "=== Checking for CAN_READ_FRAUD_ALERTS in permissions.decorator.ts ==="
rg -n "CAN_READ_FRAUD" backend/src/auth/decorators/permissions.decorator.ts
echo ""
echo "=== Listing all defined permissions ==="
rg -n "CAN_" backend/src/auth/decorators/permissions.decorator.tsRepository: OlufunbiIK/StellarSplit Length of output: 2210 🏁 Script executed: # View the fraud-detection.controller.ts file to see the actual code
echo "=== Viewing fraud-detection.controller.ts (lines 1-130) ==="
head -n 130 backend/src/fraud-detection/fraud-detection.controller.ts | cat -nRepository: OlufunbiIK/StellarSplit Length of output: 4246 🏁 Script executed: # Search for any fraud-related permissions anywhere in the codebase
echo "=== Searching for fraud permissions definitions ==="
rg -i "fraud" backend/src/auth --type ts
echo ""
echo "=== Searching for CAN_READ_FRAUD_ALERTS anywhere ==="
rg "CAN_READ_FRAUD_ALERTS" backend/Repository: OlufunbiIK/StellarSplit Length of output: 287 🏁 Script executed: # Check the AuthorizationGuard to see supported resource types
echo "=== Checking AuthorizationGuard implementation ==="
rg -A 20 "checkPermission" backend/src/auth/guards/authorization.guard.ts | head -n 50Repository: OlufunbiIK/StellarSplit Length of output: 984 Critical: Multiple permission and authorization gaps in fraud detection endpoints.
Add fraud-related permissions to the 🤖 Prompt for AI Agents |
||
| async getAlerts( | ||
| @Query("status") status?: AlertStatus, | ||
| @Query("page") page: number = 1, | ||
|
|
@@ -45,6 +51,7 @@ export class FraudDetectionController { | |
| * Get a single alert | ||
| */ | ||
| @Get("alerts/:id") | ||
| @RequirePermissions(Permissions.CAN_READ_SPLIT) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: Using split permissions for fraud resources causes authorization failures.
For fraud endpoints, this will either:
Define dedicated fraud permissions with a new Also applies to: 67-67, 81-81, 90-90 🤖 Prompt for AI Agents |
||
| async getAlert(@Param("id") id: string) { | ||
| const alert = await this.fraudDetectionService.getAlert(id); | ||
| if (!alert) { | ||
|
|
@@ -57,6 +64,7 @@ export class FraudDetectionController { | |
| * Resolve a fraud alert | ||
| */ | ||
| @Post("alerts/:id/resolve") | ||
| @RequirePermissions(Permissions.CAN_UPDATE_SPLIT) | ||
| @HttpCode(HttpStatus.OK) | ||
| async resolveAlert( | ||
| @Param("id") id: string, | ||
|
|
@@ -70,6 +78,7 @@ export class FraudDetectionController { | |
| * Get analysis for a specific split | ||
| */ | ||
| @Get("splits/:id/analysis") | ||
| @RequirePermissions(Permissions.CAN_READ_SPLIT) | ||
| async getSplitAnalysis(@Param("id") id: string) { | ||
| return this.fraudDetectionService.getSplitAnalysis(id); | ||
| } | ||
|
|
@@ -78,6 +87,7 @@ export class FraudDetectionController { | |
| * Get fraud detection statistics | ||
| */ | ||
| @Get("stats") | ||
| @RequirePermissions(Permissions.CAN_READ_SPLIT) | ||
| async getStats() { | ||
| return this.fraudDetectionService.getStats(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -320,16 +320,8 @@ export class FraudDetectionService { | |||||||||||||||||||||
| where: { status: AlertStatus.FALSE_POSITIVE }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Calculate accuracy | ||||||||||||||||||||||
| const reviewed = await this.fraudAlertRepository.count({ | ||||||||||||||||||||||
| where: [{ status: AlertStatus.RESOLVED }, { status: AlertStatus.FALSE_POSITIVE }], | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const truePositives = await this.fraudAlertRepository.count({ | ||||||||||||||||||||||
| where: { is_true_positive: true }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const accuracy = reviewed > 0 ? (truePositives + falsePositives) / reviewed : 0; | ||||||||||||||||||||||
| // Calculate accuracy as precision: true positives / (true positives + false positives) | ||||||||||||||||||||||
| const accuracy = reviewed > 0 ? truePositives / reviewed : 0; | ||||||||||||||||||||||
|
Comment on lines
+323
to
+324
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Undefined variables Lines 323-324 reference This will throw a 🐛 Proposed fix to define the missing variables const falsePositives = await this.fraudAlertRepository.count({
where: { status: AlertStatus.FALSE_POSITIVE },
});
+ // True positives are resolved alerts that were confirmed as actual fraud
+ const truePositives = await this.fraudAlertRepository.count({
+ where: { status: AlertStatus.RESOLVED, is_true_positive: true },
+ });
+ const reviewed = truePositives + falsePositives;
+
// Calculate accuracy as precision: true positives / (true positives + false positives)
const accuracy = reviewed > 0 ? truePositives / reviewed : 0;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return { | ||||||||||||||||||||||
| totalAlerts: total, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assignCategorydoesn't verify user owns the split.The endpoint requires
CAN_UPDATE_SPLITpermission but doesn't verify the authenticated user actually owns/has access to the split being modified. This could allow a user to modify category assignments on other users' splits.🔧 Proposed fix
`@Put`('splits/:splitId/category') `@RequirePermissions`(Permissions.CAN_UPDATE_SPLIT) - async assignCategory(`@Param`('splitId') splitId: string, `@Body`('categoryId') categoryId: string) { - return this.complianceService.assignCategoryToSplit(splitId, categoryId); + async assignCategory( + `@Param`('splitId') splitId: string, + `@Body`('categoryId') categoryId: string, + `@Req`() req: AuthRequest, + ) { + return this.complianceService.assignCategoryToSplit(splitId, categoryId, req.user.walletAddress); }The service method should then verify the split belongs to the user.
🤖 Prompt for AI Agents