import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, Req, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../common/guards/roles.guard'; import { Roles } from '../common/decorators/roles.decorator'; import { AdminService } from './admin.service'; import { CopyCatalogDto, DryRunDto, PurgeDto, ResetPasswordDto, ResetVenueDto, TestEmailDto, UpdateRoleDto, VenueNameDto, } from './dto/admin.dto'; /** The superadmin page: every venue, terminal and user of KoolTap. Destructive actions take a dryRun to preview. */ @ApiTags('Admin') @ApiBearerAuth() @UseGuards(JwtAuthGuard, RolesGuard) @Roles('superadmin') @Controller('admin') export class AdminController { constructor(private admin: AdminService) {} // ---- Venues @Get('venues') @ApiOperation({ summary: 'Every venue with what it holds' }) listVenues() { return this.admin.listVenues(); } @Post('venues') @ApiOperation({ summary: 'Create a venue' }) createVenue(@Body() dto: VenueNameDto, @Req() req: any) { return this.admin.createVenue(dto.name, req.user); } @Patch('venues/:venueId') @ApiOperation({ summary: 'Rename a venue' }) renameVenue(@Param('venueId') venueId: string, @Body() dto: VenueNameDto, @Req() req: any) { return this.admin.renameVenue(venueId, dto.name, req.user); } @Delete('venues/:venueId') @ApiOperation({ summary: 'Delete a venue with everything in it, users and terminals included' }) @ApiQuery({ name: 'confirmName', description: "The venue's name" }) deleteVenue(@Param('venueId') venueId: string, @Query('confirmName') confirmName: string | undefined, @Req() req: any) { return this.admin.deleteVenue(venueId, confirmName, req.user); } @Post('venues/:venueId/view') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Start viewing the dashboard as the venue; recorded in the venue's audit log" }) viewVenue(@Param('venueId') venueId: string, @Req() req: any) { return this.admin.viewVenue(venueId, req.user); } @Post('venues/:venueId/copy-catalog') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Copy another venue's products that this venue doesn't have yet" }) copyCatalog(@Param('venueId') venueId: string, @Body() dto: CopyCatalogDto, @Req() req: any) { return this.admin.copyCatalog(venueId, dto.fromVenueId, req.user); } @Post('venues/:venueId/reset') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Wipe parts of a venue's data; users, terminals and products stay" }) resetVenue(@Param('venueId') venueId: string, @Body() dto: ResetVenueDto, @Req() req: any) { return this.admin.resetVenue(venueId, dto.scopes, { stockValue: dto.stockValue, dryRun: dto.dryRun }, req.user); } @Get('venues/:venueId/consistency') @ApiOperation({ summary: "Look for data in the venue that doesn't add up" }) consistency(@Param('venueId') venueId: string) { return this.admin.consistency(venueId); } @Get('venues/:venueId/export') @ApiOperation({ summary: 'Everything of the venue as JSON, without passwords and pictures' }) exportVenue(@Param('venueId') venueId: string, @Req() req: any) { return this.admin.exportVenue(venueId, req.user); } @Post('venues/:venueId/registration-codes') @ApiOperation({ summary: 'A pairing code for a terminal of the venue' }) createRegistrationCode(@Param('venueId') venueId: string, @Req() req: any) { return this.admin.createRegistrationCode(venueId, req.user); } // ---- Terminals @Get('devices') @ApiOperation({ summary: 'Every terminal, unpaired ones included' }) listDevices() { return this.admin.listDevices(); } @Post('devices/:deviceId/revoke') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Unpair a terminal' }) revokeDevice(@Param('deviceId') deviceId: string, @Req() req: any) { return this.admin.revokeDevice(deviceId, req.user); } @Get('registration-codes') @ApiOperation({ summary: 'Pairing codes that can still be used' }) listRegistrationCodes() { return this.admin.listRegistrationCodes(); } @Post('registration-codes/:id/revoke') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Make a pairing code unusable' }) revokeRegistrationCode(@Param('id') id: string) { return this.admin.revokeRegistrationCode(id); } @Post('registration-codes/cleanup') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Delete used and expired pairing codes' }) cleanupRegistrationCodes() { return this.admin.cleanupRegistrationCodes(); } @Post('commands/expire-stuck') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Expire every pending command' }) expireStuckCommands(@Body() dto: DryRunDto, @Req() req: any) { return this.admin.expireStuckCommands(dto.dryRun ?? false, req.user); } // ---- Users @Get('users') @ApiOperation({ summary: 'Every dashboard user' }) listUsers() { return this.admin.listUsers(); } @Patch('users/:userId/role') @ApiOperation({ summary: "Change a user's role (not your own)" }) updateRole(@Param('userId') userId: string, @Body() dto: UpdateRoleDto, @Req() req: any) { return this.admin.updateRole(userId, dto.role, req.user); } @Post('users/:userId/password') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Set a user's password; signs them out everywhere" }) resetPassword(@Param('userId') userId: string, @Body() dto: ResetPasswordDto, @Req() req: any) { return this.admin.resetPassword(userId, dto.password, req.user); } @Post('users/:userId/logout') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Sign a user out everywhere, at once' }) logoutUser(@Param('userId') userId: string, @Req() req: any) { return this.admin.logoutUser(userId, req.user); } @Delete('users/:userId') @ApiOperation({ summary: 'Delete a user (not yourself)' }) deleteUser(@Param('userId') userId: string, @Req() req: any) { return this.admin.deleteUser(userId, req.user); } @Post('sessions/revoke-all') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Sign out every user but yourself' }) logoutEveryone(@Req() req: any) { return this.admin.logoutEveryone(req.user); } // ---- Maintenance @Post('purge') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Delete old terminal events, audit entries or finished commands' }) purge(@Body() dto: PurgeDto, @Req() req: any) { return this.admin.purge(dto.targets, dto.olderThanDays, dto.venueId, dto.dryRun ?? false, req.user); } @Post('images/cleanup') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Delete pictures no product uses (older than a week)' }) cleanupImages(@Body() dto: DryRunDto, @Req() req: any) { return this.admin.cleanupImages(dto.dryRun ?? false, req.user); } @Post('email/test') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Send the test email to an address' }) sendTestEmail(@Body() dto: TestEmailDto, @Req() req: any) { return this.admin.sendTestEmail(dto.to, req.user); } // ---- Insight @Get('audit') @ApiOperation({ summary: 'The audit log of every venue' }) @ApiQuery({ name: 'venueId', required: false }) @ApiQuery({ name: 'action', required: false, description: 'Part of the action name' }) @ApiQuery({ name: 'limit', required: false }) @ApiQuery({ name: 'offset', required: false }) auditLog( @Query('venueId') venueId?: string, @Query('action') action?: string, @Query('limit') limit?: string, @Query('offset') offset?: string, ) { return this.admin.auditLog({ venueId: venueId || undefined, action: action || undefined, limit: Math.min(Math.max(parseInt(limit ?? '', 10) || 50, 1), 200), offset: Math.max(parseInt(offset ?? '', 10) || 0, 0), }); } @Get('health') @ApiOperation({ summary: 'Database, migrations, live connections and background jobs' }) health() { return this.admin.health(); } }