SmartfolioBackend / src /modules /verification /verification.controller.ts
ibrahimgulbutt's picture
deploy: appeals on connection checks
614ff63
Raw History Blame Contribute Delete
3.32 kB
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);
}
}