import { Controller, Post, Get, Param, Body, UseGuards, HttpCode, HttpStatus, Req, Query, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { SkipThrottle, Throttle } from '@nestjs/throttler'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiConsumes } from '@nestjs/swagger'; import { ImagesService } from '../images/images.service'; import { MAX_UPLOAD_BYTES } from '../images/image-processing'; import { DevicesService } from './devices.service'; import { AuthService } from '../auth/auth.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { DeviceAuthGuard } from '../auth/guards/device-auth.guard'; import { AppVersionGuard } from './app-version.guard'; import { RolesGuard } from '../common/guards/roles.guard'; import { Roles } from '../common/decorators/roles.decorator'; import { RegisterDeviceDto } from './dto/register-device.dto'; import { SyncDto } from './dto/sync.dto'; import { AckCommandDto } from './dto/ack-command.dto'; @ApiTags('Devices') @Controller('devices') export class DevicesController { constructor( private devicesService: DevicesService, private authService: AuthService, private images: ImagesService, ) {} @Post('register') @Throttle({ default: { limit: 10, ttl: 60000 } }) @UseGuards(AppVersionGuard) @ApiOperation({ summary: 'Register a new device' }) @ApiResponse({ status: 201, description: 'Device registered successfully' }) async register(@Body() dto: RegisterDeviceDto) { return this.authService.registerDevice(dto.venueId, dto.name, dto.registrationCode); } // Authenticated by the device token; the till must never be rate-limited out by other // traffic from the venue's shared IP @Post(':deviceId/sync') @SkipThrottle() @UseGuards(AppVersionGuard, DeviceAuthGuard) @ApiOperation({ summary: 'Sync device state with server' }) @ApiResponse({ status: 200, description: 'Sync successful, returns pending commands' }) async sync(@Param('deviceId') deviceId: string, @Body() dto: SyncDto, @Req() req: any) { return this.devicesService.sync(req.user, dto); } // A picture taken on a terminal. Only stored: the terminal links it to its product with a product_updated event, // so an offline edit and its picture follow the same rules as every other change @Post(':deviceId/images') @SkipThrottle() @UseGuards(AppVersionGuard, DeviceAuthGuard) @UseInterceptors(FileInterceptor('file', { limits: { fileSize: MAX_UPLOAD_BYTES } })) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Upload a product picture from a terminal' }) async uploadImage(@UploadedFile() file: Express.Multer.File | undefined) { return { imageId: await this.images.store(file?.buffer) }; } @Post(':deviceId/commands/:commandId/ack') @SkipThrottle() @UseGuards(AppVersionGuard, DeviceAuthGuard) @ApiOperation({ summary: 'Acknowledge a command result' }) @ApiResponse({ status: 200, description: 'Command acknowledged' }) async ackCommand( @Param('deviceId') deviceId: string, @Param('commandId') commandId: string, @Body() dto: AckCommandDto, @Req() req: any, ) { return this.devicesService.ackCommand(req.user, commandId, dto); } @Get('generation-code') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin') @ApiBearerAuth() @ApiOperation({ summary: 'Generate a registration code for pairing' }) @ApiResponse({ status: 200, description: 'Registration code generated' }) async generateCode(@Req() req: any, @Query('venueId') viewing?: string) { // A superadmin pairs terminals for the venue they're viewing const venueId = req.user.role === 'superadmin' && viewing ? viewing : req.user.venueId; return this.authService.generateRegistrationCode(venueId); } }