import { Body, Controller, Get, NotFoundException, Param, ParseUUIDPipe, Patch, Query, UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { VerificationService } from './verification.service'; import { ResolveAppealDto } from './dto/appeal.dto'; import { DeviceSignatureService } from './device-signature.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { RolesGuard } from '../auth/roles.guard'; import { Roles } from '../auth/roles.decorator'; @ApiTags('Verification (admin)') @ApiBearerAuth() @Controller('admin/verification') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin') export class VerificationAdminController { constructor( private readonly verification: VerificationService, private readonly deviceSignatures: DeviceSignatureService, ) {} /** * Devices seen under more than one account. * * A lead list for a human, never an enforcement queue. Internet cafes, family * computers and shared office machines all land here legitimately, so a * cluster is a reason to look, not a reason to act. */ @Get('clusters') @ApiOperation({ summary: 'Devices seen under more than one account' }) async clusters(@Query('limit') limit?: string) { const clusters = await this.deviceSignatures.sharedClusters( Math.min(Number(limit) || 100, 200), ); return { count: clusters.length, clusters }; } @Get('sessions') @ApiOperation({ summary: 'Recent checks, newest first' }) async sessions(@Query('verdict') verdict?: string, @Query('limit') limit?: string) { const rows = await this.verification.recent({ verdict, limit: Number(limit) || 50 }); return { count: rows.length, sessions: rows, lists: this.verification.listStatus() }; } /* * Before @Get(':id') on purpose. Nest matches in declaration order, and a * literal path registered after a parameterised one is unreachable — * 'appeals' would be read as an id and fail UUID validation. */ @Get('appeals') @ApiOperation({ summary: 'Checks the candidate says got it wrong' }) async appeals(@Query('status') status?: string) { const wanted = status === 'accepted' || status === 'declined' || status === 'all' ? status : 'open'; const rows = await this.verification.appeals(wanted); return { count: rows.length, appeals: rows }; } @Patch(':id/appeal') @ApiOperation({ summary: 'Decide one dispute' }) async resolveAppeal( @Param('id', ParseUUIDPipe) id: string, @Body() dto: ResolveAppealDto, ) { return this.verification.resolveAppeal(id, dto); } @Get(':id') @ApiOperation({ summary: 'One check in full' }) async detail(@Param('id', ParseUUIDPipe) id: string) { const session = await this.verification.findOne(id); if (!session) throw new NotFoundException('Verification session not found.'); return session; } }