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;
  }
}