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:
@@ -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) {
|
||||
|
||||
@@ -4,10 +4,11 @@ import { Repository } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { Application } from './entities/application.entity';
|
||||
import { CreateApplicationDto, UpdateApplicationDto } from './dto/application.dto';
|
||||
import { ClustersService } from '../clusters/clusters.service';
|
||||
import { UserRole } from '../common/enums';
|
||||
import { UserRole, DatabaseType } from '../common/enums';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationsService {
|
||||
@@ -66,11 +67,22 @@ export class ApplicationsService {
|
||||
this.logger.log(`Manual cluster assignment for app "${dto.name}" → cluster ${clusterId}`);
|
||||
}
|
||||
|
||||
// Generate database credentials if a database is requested
|
||||
let dbUsername: string | undefined;
|
||||
let dbPassword: string | undefined;
|
||||
if (dto.databaseType && dto.databaseType !== DatabaseType.NONE) {
|
||||
dbUsername = dto.dbUsername?.trim() || 'appuser';
|
||||
dbPassword = dto.dbPassword?.trim() || crypto.randomBytes(16).toString('hex');
|
||||
this.logger.log(`Generated DB credentials for app "${dto.name}" — user: ${dbUsername}`);
|
||||
}
|
||||
|
||||
const app = this.appsRepository.create({
|
||||
...dto,
|
||||
userId,
|
||||
clusterId,
|
||||
poolId,
|
||||
dbUsername,
|
||||
dbPassword,
|
||||
subdomain: `${dto.name}-${userId.split('-')[0]}`,
|
||||
});
|
||||
return this.appsRepository.save(app);
|
||||
|
||||
@@ -32,6 +32,16 @@ export class CreateApplicationDto {
|
||||
@IsEnum(DatabaseType)
|
||||
databaseType: DatabaseType;
|
||||
|
||||
@ApiPropertyOptional({ example: 'appuser', description: 'Database username (default: appuser)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dbUsername?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'mySecurePass123', description: 'Database password (auto-generated if empty)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dbPassword?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'https://github.com/user/repo.git' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -29,6 +29,12 @@ export class Application {
|
||||
@Column({ type: 'enum', enum: DatabaseType, default: DatabaseType.NONE })
|
||||
databaseType: DatabaseType;
|
||||
|
||||
@Column({ nullable: true })
|
||||
dbUsername: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
dbPassword: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
gitUrl: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user