Files
kooltab-backoffice/apps/api/src/admin/admin.controller.ts
T
brammie15 4e43521da7
CI / build-and-test (push) Canceled after 0s
feat: add superadmin
2026-10-26 11:58:10 +01:00

227 lines
7.9 KiB
TypeScript

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