feat: database management — custom credentials, dump upload/restore

Backend:
- Add dbUsername/dbPassword columns to Application entity
- Add optional DB credential fields to CreateApplicationDto
- Auto-generate dbPassword (crypto.randomBytes) and default dbUsername='appuser'
  when databaseType != 'none' on app creation
- Store both username and password in K8s DB secret (was password-only)
- Read DB_USER/POSTGRES_USER/MYSQL_USER from secretKeyRef instead of hardcoded
- New restoreDatabaseDump() in KubernetesService: creates K8s Job with
  psql/mysql client to restore uploaded SQL dump, waits for completion,
  returns logs
- New POST /applications/:id/db-upload endpoint with 500MB file limit

Frontend:
- Add dbUsername/dbPassword to Application and CreateApplicationDto types
- Deploy page: show username/password fields when database is selected,
  with generate-random-password button and show/hide toggle
- App detail page: new Database section with connection info (host, port,
  db name, username, password with copy-to-clipboard), SQL dump upload
  area with drag-and-drop, and restore output logs display

Security:
- Database remains ClusterIP only (no external exposure)
- Credentials stored in K8s Secrets (base64-encoded)
- Dump file uploaded as temporary K8s Secret, auto-cleaned after restore
This commit is contained in:
keyhan
2026-04-06 22:47:41 +03:30
parent 3c3e0e48fa
commit 9e3347cb71
9 changed files with 496 additions and 13 deletions
@@ -14,6 +14,7 @@ import {
Logger,
Inject,
forwardRef,
BadRequestException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
@@ -22,7 +23,7 @@ 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 } from '../common/enums';
import { UserRole, DatabaseType } from '../common/enums';
import { KubernetesService } from '../kubernetes/kubernetes.service';
import { DeploymentsService } from '../deployments/deployments.service';
@@ -60,6 +61,38 @@ export class ApplicationsController {
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()
@ApiOperation({ summary: 'List my applications' })
async findAll(@Request() req: any) {