diff --git a/apps/backend/bun.lock b/apps/backend/bun.lock index 167e10a..9773556 100644 --- a/apps/backend/bun.lock +++ b/apps/backend/bun.lock @@ -21,7 +21,7 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", "ts-node": "^10.9.2", - "typescript": "5.8.3", + "typescript": "^5.9.2", "typescript-eslint": "^8.30.0", }, "peerDependencies": { diff --git a/apps/backend/index.ts b/apps/backend/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/apps/backend/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index 609b209..ec357f1 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -22,7 +22,7 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", "ts-node": "^10.9.2", - "typescript": "5.8.3", + "typescript": "^5.8.3", "typescript-eslint": "^8.30.0" }, "private": true, @@ -30,7 +30,10 @@ "dev": "bun run --watch src/index.ts", "build": "bun build src/index.ts --outdir ./dist --target node", "lint": "bun eslint src/**/*.ts", - "format": "bun prettier --write 'src/**/*.ts'" + "format": "bun prettier --write 'src/**/*.ts'", + "setup": "bun run scripts/setup.ts", + "clean": "bun run scripts/clean.ts", + "command-executor": "bun run scripts/command-executor.ts" }, "type": "module" } diff --git a/apps/backend/scripts/clean.ts b/apps/backend/scripts/clean.ts new file mode 100644 index 0000000..7e8a815 --- /dev/null +++ b/apps/backend/scripts/clean.ts @@ -0,0 +1,15 @@ +import { fileManager } from '../src/utils/fileManager'; + +async function main() { + const customPath = process.argv[2]; + + try { + await fileManager.cleanupProject(customPath); + console.log(' Cleanup completed successfully'); + } catch (error) { + console.error(' Cleanup failed:', error instanceof Error ? error.message : error); + process.exit(1); + } +} + +await main(); \ No newline at end of file diff --git a/apps/backend/scripts/command-executor.ts b/apps/backend/scripts/command-executor.ts new file mode 100644 index 0000000..d947845 --- /dev/null +++ b/apps/backend/scripts/command-executor.ts @@ -0,0 +1,58 @@ +import { executeCommand } from "../src/utils/commandExecutor"; + +interface CommandError extends Error { + stderr?: string; + stdout?: string; + code?: number; +} + +async function main() { + try { + console.log('Testing command executor...\n'); + + // Test 1: Simple successful command + console.log('Test 1: Running "echo hello world"'); + const result1 = await executeCommand('echo "hello world"'); + console.log('Success:', result1, '\n'); + + // Test 2: Command with error + console.log('Test 2: Running "ls non-existent-file"'); + try { + await executeCommand('ls non-existent-file'); + } catch (error: unknown) { + const err = error as CommandError; + console.log('Error caught as expected:'); + console.log('Message:', err.message); + if (err.stderr) console.log('Stderr:', err.stderr); + console.log(''); + } + + // Test 3: Command with timeout (using macOS compatible command) + console.log('Test 3: Running "ping -c 5 127.0.0.1" with 1000ms timeout'); + try { + await executeCommand('ping -c 5 127.0.0.1', 1000); + } catch (error: unknown) { + const err = error as CommandError; + console.log('Timeout caught as expected:'); + console.log('Message:', err.message); + console.log(''); + } + + // Test 4: Successful command sequence + console.log('Test 4: Running "date && whoami"'); + const result4 = await executeCommand('date && whoami'); + console.log('Success:', result4); + + console.log('\nAll tests completed successfully!'); + + } catch (error: unknown) { + const err = error as CommandError; + console.error('\nUnexpected error in test script:'); + console.error('Message:', err.message); + if (err.stderr) console.error('Stderr:', err.stderr); + if (err.stdout) console.error('Stdout:', err.stdout); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/apps/backend/scripts/setup.ts b/apps/backend/scripts/setup.ts new file mode 100644 index 0000000..ed6b364 --- /dev/null +++ b/apps/backend/scripts/setup.ts @@ -0,0 +1,17 @@ +import { fileManager } from '../src/utils/fileManager'; + +async function main() { + try { + const projectPath = await fileManager.setupProject(); + console.log('Project created at:', projectPath); + console.log('\nTo clean up later, run:'); + console.log('bun run clean'); + console.log('or to clean a specific path:'); + console.log('bun run clean -- /path/to/project'); + } catch (error) { + console.error('Setup failed:', error instanceof Error ? error.message : error); + process.exit(1); + } +} + +await main(); \ No newline at end of file diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 663963d..6ad80b8 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,9 +1,46 @@ import express from 'express'; +import helmet from 'helmet'; +import cors from 'cors'; import { setupProject, getSanitizedDirName, createRustProject } from './utils/fileManager'; const app = express(); + +// Security middleware +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", 'data:', 'https:'], + }, + }, + crossOriginEmbedderPolicy: false, + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true, + }, + }) +); + +// CORS configuration +app.use( + cors({ + origin: 'http://localhost:4200', + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], + optionsSuccessStatus: 200, + }) +); + +// Body parsing middleware app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +// Routes app.get('/', (_, res) => res.send('Hello from Backend!' + '
' + 'The best online soroban compiler is coming...') ); @@ -45,4 +82,24 @@ app.post('/api/test-filemanager', async (req, res) => { } }); -app.listen(3000, () => console.log('Server on http://localhost:3000')); +// Error handling middleware +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + res.status(500).json({ + error: 'Internal Server Error', + message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong', + }); +}); + +// 404 handler +app.use((req, res) => { + res.status(404).json({ + error: 'Not Found', + message: `Route ${req.originalUrl} not found`, + }); +}); + +// Start server +app.listen(3000, () => { + console.log('Server on http://localhost:3000'); + console.log('CORS restricted to http://localhost:4200'); +}); diff --git a/apps/backend/src/utils/commandExecutor.ts b/apps/backend/src/utils/commandExecutor.ts index a113487..95dcdf5 100644 --- a/apps/backend/src/utils/commandExecutor.ts +++ b/apps/backend/src/utils/commandExecutor.ts @@ -1,164 +1,72 @@ -import { spawn, ChildProcess } from 'child_process'; +import { spawn, type SpawnOptionsWithoutStdio } from 'child_process'; -/** - * Result of a command execution - */ -export interface CommandResult { - /** Exit code of the command */ - exitCode: number; - /** Standard output */ - stdout: string; - /** Standard error output */ - stderr: string; - /** Whether the command was killed due to timeout */ - timedOut: boolean; -} +const DEFAULT_TIMEOUT = 30000; /** - * Options for command execution - */ -export interface ExecuteOptions { - /** Working directory for the command */ - cwd?: string; - /** Environment variables */ - env?: Record; - /** Timeout in milliseconds (default: 30000ms = 30s) */ - timeout?: number; -} - -/** - * Error thrown when a command exceeds the timeout limit - */ -export class CommandTimeoutError extends Error { - constructor(timeout: number) { - super(`Command exceeded time limit of ${timeout}ms`); - this.name = 'CommandTimeoutError'; - } -} - -/** - * Executes a shell command with timeout enforcement - * - * @param command - The command to execute - * @param args - Arguments for the command - * @param options - Execution options including timeout - * @returns Promise that resolves to CommandResult - * @throws CommandTimeoutError if command exceeds timeout + * Executes a shell command securely with a timeout + * @param command The command to execute + * @param timeout Maximum execution time in milliseconds (default: 30000) + * @returns Promise that resolves with the command output + * @throws Error with stderr content if command fails or times out */ export async function executeCommand( command: string, - args: string[] = [], - options: ExecuteOptions = {} -): Promise { - const { cwd, env = process.env, timeout = 30000 } = options; - - return new Promise((resolve, reject) => { - // Buffer to collect stdout and stderr - let stdout = ''; - let stderr = ''; - let timedOut = false; - let childProcess: ChildProcess; + timeout: number = DEFAULT_TIMEOUT +): Promise { + // Validate inputs + if (typeof command !== 'string' || command.trim() === '') { + throw new Error('Command must be a non-empty string'); + } - try { - // Spawn the child process - childProcess = spawn(command, args, { - cwd, - env: { ...process.env, ...env }, - stdio: ['pipe', 'pipe', 'pipe'], - }); + if (typeof timeout !== 'number' || timeout <= 0) { + throw new Error('Timeout must be a positive number'); + } - // Set up timeout - const timeoutId = setTimeout(() => { - timedOut = true; + const options: SpawnOptionsWithoutStdio = { + shell: '/bin/bash', + env: { ...process.env }, + }; - // Kill the process if it's still running - if (childProcess && !childProcess.killed) { - childProcess.kill('SIGTERM'); + return new Promise((resolve, reject) => { + const child = spawn('bash', ['-c', command], options); - // Force kill after 5 seconds if SIGTERM doesn't work - setTimeout(() => { - if (childProcess && !childProcess.killed) { - childProcess.kill('SIGKILL'); - } - }, 5000); - } + let stdout = ''; + let stderr = ''; + let timeoutId: NodeJS.Timeout; - reject(new CommandTimeoutError(timeout)); + // Set up timeout + if (timeout !== Infinity) { + timeoutId = setTimeout(() => { + child.kill('SIGTERM'); + reject(new Error(`Command timed out after ${timeout}ms`)); }, timeout); + } - // Collect stdout data - if (childProcess.stdout) { - childProcess.stdout.on('data', (data: Buffer) => { - stdout += data.toString(); - }); - } - - // Collect stderr data - if (childProcess.stderr) { - childProcess.stderr.on('data', (data: Buffer) => { - stderr += data.toString(); - }); - } - - // Handle process completion - childProcess.on('close', (exitCode: number | null) => { - clearTimeout(timeoutId); - - // Don't resolve if we already timed out - if (timedOut) { - return; - } - - resolve({ - exitCode: exitCode ?? -1, - stdout: stdout.trim(), - stderr: stderr.trim(), - timedOut: false, - }); - }); + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); - // Handle process errors - childProcess.on('error', (error: Error) => { - clearTimeout(timeoutId); + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); - // Don't reject if we already timed out - if (timedOut) { - return; - } + child.on('close', (code) => { + clearTimeout(timeoutId); + if (code === 0) { + resolve(stdout.trim()); + } else { + const error = new Error(stderr.trim() || `Command failed with exit code ${code}`); + (error as any).stderr = stderr.trim(); + (error as any).stdout = stdout.trim(); + (error as any).code = code; reject(error); - }); - - // Handle process being killed - childProcess.on('exit', (code: number | null, signal: string | null) => { - if (signal === 'SIGTERM' || signal === 'SIGKILL') { - clearTimeout(timeoutId); + } + }); - // This was likely our timeout kill, but check the flag to be sure - if (timedOut) { - return; // The timeout handler will reject - } - } - }); - } catch (error) { - reject(error); - } + child.on('error', (err) => { + clearTimeout(timeoutId); + reject(err); + }); }); } - -/** - * Executes a command with a specific timeout and returns the result - * This is a convenience wrapper around executeCommand - * - * @param command - The command to execute - * @param args - Arguments for the command - * @param timeoutMs - Timeout in milliseconds - * @returns Promise that resolves to CommandResult - */ -export async function executeCommandWithTimeout( - command: string, - args: string[] = [], - timeoutMs: number = 30000 -): Promise { - return executeCommand(command, args, { timeout: timeoutMs }); -}