import { Controller, Get, Post, Patch, Delete, Body, Param, Query, UseGuards, Request, UseInterceptors, UploadedFile, Logger, Inject, forwardRef, BadRequestException, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiConsumes } from '@nestjs/swagger'; import { ApplicationsService } from './applications.service'; import { CreateApplicationDto, UpdateApplicationDto, ScaleResourcesDto } from './dto/application.dto'; import { RolesGuard } from '../common/guards/roles.guard'; import { Roles } from '../common/decorators/roles.decorator'; import { UserRole, DatabaseType } from '../common/enums'; import { KubernetesService } from '../kubernetes/kubernetes.service'; import { DeploymentsService } from '../deployments/deployments.service'; @ApiTags('Applications') @ApiBearerAuth() @Controller('applications') @UseGuards(AuthGuard('jwt'), RolesGuard) export class ApplicationsController { private readonly logger = new Logger(ApplicationsController.name); constructor( private readonly applicationsService: ApplicationsService, private readonly kubernetesService: KubernetesService, @Inject(forwardRef(() => DeploymentsService)) private readonly deploymentsService: DeploymentsService, ) {} @Post() @ApiOperation({ summary: 'Create a new application' }) async create(@Request() req: any, @Body() dto: CreateApplicationDto) { return this.applicationsService.create(req.user.id, dto, req.user.role); } @Post(':id/upload') @ApiOperation({ summary: 'Upload application code (zip file)' }) @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 200 * 1024 * 1024 }, // 200MB (WordPress sites can be large) })) async uploadCode( @Param('id') id: string, @Request() req: any, @UploadedFile() file: Express.Multer.File, ) { return this.applicationsService.uploadCode(id, req.user.id, file); } @Post(':id/db-upload') @ApiOperation({ summary: 'Upload and restore a SQL dump into the application database' }) @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 500 * 1024 * 1024 }, // 500MB for DB dumps })) async uploadDbDump( @Param('id') id: string, @Request() req: any, @UploadedFile() file: Express.Multer.File, ) { if (!file) { throw new BadRequestException('No file uploaded'); } const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL; const app = await this.applicationsService.findOne(id, isStaff ? undefined : req.user.id); if (app.databaseType === DatabaseType.NONE) { throw new BadRequestException('This application does not have a database configured'); } this.logger.log(`DB dump upload for ${app.name} — ${(file.size / 1024).toFixed(1)} KB`); const result = await this.kubernetesService.restoreDatabaseDump(app, file.buffer); return { success: result.success, message: result.success ? 'Database restored successfully' : 'Database restore failed', logs: result.logs, }; } @Get(':id/db-storage') @ApiOperation({ summary: 'Get current database PVC storage size' }) async getDbStorage(@Param('id') id: string, @Request() req: any) { const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL; const app = await this.applicationsService.findOne(id, isStaff ? undefined : req.user.id); if (app.databaseType === DatabaseType.NONE) { throw new BadRequestException('This application does not have a database configured'); } const currentSize = await this.kubernetesService.getDatabasePvcSize(app); return { currentSize, savedSize: app.dbStorageSize || '1Gi' }; } @Patch(':id/db-storage') @ApiOperation({ summary: 'Resize (expand) database PVC storage' }) async resizeDbStorage( @Param('id') id: string, @Request() req: any, @Body() body: { size: string }, ) { const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL; const app = await this.applicationsService.findOne(id, isStaff ? undefined : req.user.id); if (app.databaseType === DatabaseType.NONE) { throw new BadRequestException('This application does not have a database configured'); } if (!body.size || !/^\d+Gi$/.test(body.size)) { throw new BadRequestException('Size must be in format like "1Gi", "5Gi", "10Gi"'); } const result = await this.kubernetesService.resizeDatabasePvc(app, body.size); if (result.success) { // Update the saved size in the DB await this.applicationsService.update(id, app.userId, { dbStorageSize: body.size } as any); } return result; } @Get() @ApiOperation({ summary: 'List my applications' }) async findAll(@Request() req: any) { return this.applicationsService.findAllByUser(req.user.id); } @Get('all') @Roles(UserRole.ADMIN, UserRole.TECHNICAL) @ApiOperation({ summary: 'List all applications (admin/technical)' }) async findAllAdmin(@Request() req: any, @Query('search') search?: string) { return this.applicationsService.findAll(search); } @Get(':id') @ApiOperation({ summary: 'Get application details' }) async findOne(@Param('id') id: string, @Request() req: any) { if (req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL) { return this.applicationsService.findOne(id); } return this.applicationsService.findOne(id, req.user.id); } @Patch(':id') @ApiOperation({ summary: 'Update application configuration' }) async update( @Param('id') id: string, @Request() req: any, @Body() dto: UpdateApplicationDto, ) { const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL; if (isStaff) { const app = await this.applicationsService.findOne(id); return this.applicationsService.update(id, app.userId, dto); } return this.applicationsService.update(id, req.user.id, dto); } @Get(':id/resources') @ApiOperation({ summary: 'Get real-time resource usage for an application' }) async getResources(@Param('id') id: string, @Request() req: any) { const app = await this.applicationsService.findOne( id, (req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL) ? undefined : req.user.id, ); return this.kubernetesService.getResourceUsage(app); } @Patch(':id/resources') @ApiOperation({ summary: 'Update application resources (CPU/Memory/Replicas)' }) async updateResources( @Param('id') id: string, @Request() req: any, @Body() dto: ScaleResourcesDto, ) { const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL; const app = await this.applicationsService.findOne(id, isStaff ? undefined : req.user.id); // Update in K8s (live) await this.kubernetesService.updateResources(app, dto); // Update in DB const updateFields: any = {}; if (dto.cpuRequest) updateFields.cpuRequest = dto.cpuRequest; if (dto.cpuLimit) updateFields.cpuLimit = dto.cpuLimit; if (dto.memoryRequest) updateFields.memoryRequest = dto.memoryRequest; if (dto.memoryLimit) updateFields.memoryLimit = dto.memoryLimit; if (dto.replicas !== undefined) updateFields.replicas = dto.replicas; const updated = await this.applicationsService.update(id, app.userId, updateFields); this.logger.log(`Updated resources for ${app.name}: ${JSON.stringify(dto)}`); return updated; } @Get(':id/preview') @ApiOperation({ summary: 'Get preview URL for the deployed application' }) async getPreview(@Param('id') id: string, @Request() req: any) { const app = await this.applicationsService.findOne( id, (req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL) ? undefined : req.user.id, ); return this.kubernetesService.getPreviewInfo(app); } @Delete(':id') @ApiOperation({ summary: 'Delete an application and all its resources' }) async delete(@Param('id') id: string, @Request() req: any) { const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL; // 1. Get the app first const app = await this.applicationsService.findOne(id, isStaff ? undefined : req.user.id); // 2. Delete K8s resources (deployment, service, ingress, db, secrets) try { if (app.clusterId && app.latestImageTag) { await this.kubernetesService.deleteApplication(app); this.logger.log(`Deleted K8s resources for ${app.name}`); } } catch (e: any) { this.logger.warn(`K8s cleanup failed for ${app.name}: ${e.message}`); } // 3. Delete deployment records from DB try { await this.deploymentsService.deleteAllForApplication(app.id); } catch (e: any) { this.logger.warn(`Deployment records cleanup failed for ${app.name}: ${e.message}`); } // 4. Delete app (also deletes uploaded files) await this.applicationsService.delete(id, isStaff ? app.userId : req.user.id); return { message: `Application "${app.name}" and all resources deleted` }; } }