Spaces:
Sleeping
Sleeping
File size: 2,929 Bytes
86063f3 614ff63 86063f3 614ff63 86063f3 614ff63 86063f3 614ff63 86063f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | 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;
}
}
|