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
@@ -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);