mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 23:32:19 +00:00
fix(stacks): harden stack management with security, validation, and logging (#520)
* fix(stacks): harden stack management with security fixes, validation alignment, and logging Validate WebSocket stack names with isValidStackName() to close a path-traversal gap on the /api/stacks/:stackName/logs WS endpoint. Align POST /api/stacks to use the canonical validator (allows underscores). Replace error: any catch blocks with error: unknown + type narrowing. Add cache invalidation to PUT /api/stacks/:stackName/env. Rename DELETE param from :name to :stackName for consistency. Add standard [Stacks] lifecycle logs and diagnostic [Stacks:debug] logs gated behind the Developer Mode toggle (with 5s TTL cache). Extract shared isDebugEnabled() and getErrorMessage() utilities. Frontend: roll back optimistic status on API failure, guard unsaved changes when switching stacks, pre-check duplicate names in App Store. * docs(settings): update Developer Mode description to mention debug diagnostics
This commit is contained in:
@@ -10,6 +10,8 @@ import { LogFormatter } from './LogFormatter';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
import { RegistryService } from './RegistryService';
|
||||
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
|
||||
/**
|
||||
* ComposeService - local docker compose CLI execution.
|
||||
*
|
||||
@@ -113,6 +115,9 @@ export class ComposeService {
|
||||
|
||||
async deployStack(stackName: string, ws?: WebSocket, atomic?: boolean): Promise<void> {
|
||||
const stackDir = path.join(this.baseDir, stackName);
|
||||
const debug = isDebugEnabled();
|
||||
const t0 = Date.now();
|
||||
if (debug) console.debug('[ComposeService:debug] deployStack', { stackName, stackDir, atomic });
|
||||
const sendOutput = (data: string) => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
|
||||
};
|
||||
@@ -166,6 +171,7 @@ export class ComposeService {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (debug) console.debug(`[ComposeService:debug] deployStack completed in ${Date.now() - t0}ms`, { stackName });
|
||||
} catch (deployError) {
|
||||
// Atomic: auto-rollback on failure
|
||||
if (atomic) {
|
||||
@@ -299,6 +305,9 @@ export class ComposeService {
|
||||
|
||||
async updateStack(stackName: string, ws?: WebSocket, atomic?: boolean): Promise<void> {
|
||||
const stackDir = path.join(this.baseDir, stackName);
|
||||
const debug = isDebugEnabled();
|
||||
const t0 = Date.now();
|
||||
if (debug) console.debug('[ComposeService:debug] updateStack', { stackName, stackDir, atomic });
|
||||
const sendOutput = (data: string) => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
|
||||
};
|
||||
@@ -358,6 +367,7 @@ export class ComposeService {
|
||||
}
|
||||
|
||||
sendOutput('=== Stack updated successfully ===\n');
|
||||
if (debug) console.debug(`[ComposeService:debug] updateStack completed in ${Date.now() - t0}ms`, { stackName });
|
||||
} catch (updateError) {
|
||||
// Atomic: auto-rollback on failure
|
||||
if (atomic) {
|
||||
|
||||
@@ -12,6 +12,8 @@ function getBackupBaseDir(): string {
|
||||
return path.join(dataDir, 'backups');
|
||||
}
|
||||
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
|
||||
/**
|
||||
* FileSystemService - local-only file I/O for compose stack management.
|
||||
*
|
||||
@@ -52,6 +54,7 @@ export class FileSystemService {
|
||||
const filePath = path.join(stackDir, file);
|
||||
try {
|
||||
await fsPromises.access(filePath);
|
||||
if (isDebugEnabled()) console.debug('[FileSystemService:debug] Resolved compose file', { stackName, file });
|
||||
return filePath;
|
||||
} catch {
|
||||
// continue
|
||||
@@ -249,6 +252,8 @@ export class FileSystemService {
|
||||
* holds sencho.db and encryption.key.
|
||||
*/
|
||||
async backupStackFiles(stackName: string): Promise<void> {
|
||||
const debug = isDebugEnabled();
|
||||
const t0 = Date.now();
|
||||
const stackDir = path.join(this.baseDir, stackName);
|
||||
const backupDir = path.join(getBackupBaseDir(), stackName);
|
||||
await fsPromises.mkdir(backupDir, { recursive: true });
|
||||
@@ -282,9 +287,12 @@ export class FileSystemService {
|
||||
|
||||
// Write timestamp marker
|
||||
await fsPromises.writeFile(path.join(backupDir, '.timestamp'), Date.now().toString(), 'utf-8');
|
||||
if (debug) console.debug(`[FileSystemService:debug] Backup completed in ${Date.now() - t0}ms`, { stackName });
|
||||
}
|
||||
|
||||
async restoreStackFiles(stackName: string): Promise<void> {
|
||||
const debug = isDebugEnabled();
|
||||
const t0 = Date.now();
|
||||
const stackDir = path.join(this.baseDir, stackName);
|
||||
const backupDir = path.join(getBackupBaseDir(), stackName);
|
||||
|
||||
@@ -293,6 +301,7 @@ export class FileSystemService {
|
||||
if (item === '.timestamp') continue;
|
||||
await fsPromises.copyFile(path.join(backupDir, item), path.join(stackDir, item));
|
||||
}
|
||||
if (debug) console.debug(`[FileSystemService:debug] Restore completed in ${Date.now() - t0}ms`, { stackName, files: items.filter(i => i !== '.timestamp') });
|
||||
}
|
||||
|
||||
async getBackupInfo(stackName: string): Promise<{ exists: boolean; timestamp: number | null }> {
|
||||
|
||||
Reference in New Issue
Block a user