Spaces:
Sleeping
Sleeping
File size: 3,323 Bytes
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 86 87 88 | import {
Body,
Get,
Param,
Controller,
HttpCode,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import type { Request } from 'express';
import { VerificationService } from './verification.service';
import { SubmitCheckDto } from './dto/submit-check.dto';
import { AppealDto } from './dto/appeal.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { OptionalJwtAuthGuard } from '../auth/optional-jwt-auth.guard';
import { RateLimit, RateLimitGuard } from '../../common/guards/rate-limit.guard';
import { clientIp } from '../../common/client-ip';
@ApiTags('Verification')
@Controller('verification')
export class VerificationController {
constructor(private readonly verification: VerificationService) {}
/**
* Submit a location and presence check.
*
* Deliberately reachable without a JWT: the first place this runs is the
* public apply page, before the candidate has an account. When a token IS
* present the session is attributed to that user.
*/
@Post('session')
@HttpCode(201)
@UseGuards(OptionalJwtAuthGuard, RateLimitGuard)
@RateLimit({ limit: 20, windowMs: 60_000 })
@ApiBearerAuth()
@ApiOperation({
summary: 'Run a location and presence check',
description:
'Works with or without authentication. Returns a verdict of clean, review or blocked, ' +
'plus the findings behind it. Nothing here auto-rejects a candidate: `blocked` means ' +
'something is fixable (turn off the VPN), `review` means a human should look.',
})
@ApiResponse({ status: 201, description: 'Check recorded; verdict returned.' })
@ApiResponse({ status: 429, description: 'Rate limited by IP.' })
async submit(@Body() dto: SubmitCheckDto, @Req() req: Request) {
const user = req.user as { id?: string } | undefined;
return this.verification.submit(dto, {
ip: clientIp(req),
userAgent: req.headers['user-agent'] ?? null,
acceptLanguage: req.headers['accept-language'] ?? null,
userId: user?.id ?? null,
campaignCandidateId: dto.campaignCandidateId ?? null,
campaignId: dto.campaignId ?? null,
});
}
/**
* Dispute a check that got it wrong.
*
* A real JWT, not the optional one: an appeal has to belong to somebody or it
* cannot be answered. This is only reachable because the check now runs after
* signup — before that, the people being refused had no account to appeal
* from.
*/
@Post('session/:id/appeal')
@HttpCode(200)
@UseGuards(JwtAuthGuard, RateLimitGuard)
@RateLimit({ limit: 10, windowMs: 60_000 })
@ApiBearerAuth()
@ApiOperation({ summary: 'Tell us this check got it wrong' })
async appeal(@Param('id') id: string, @Body() dto: AppealDto, @Req() req: Request) {
const user = req.user as { id: string };
return this.verification.appeal(id, user.id, dto);
}
/** Whether this check can still be disputed, and what was already said. */
@Get('session/:id/appeal')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'The state of my dispute on one check' })
async myAppeal(@Param('id') id: string, @Req() req: Request) {
const user = req.user as { id: string };
return this.verification.myAppeal(id, user.id);
}
}
|