mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,10 @@
|
||||
* Common utilities for fs routes
|
||||
*/
|
||||
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import {
|
||||
getErrorMessage as getErrorMessageShared,
|
||||
createLogError,
|
||||
} from "../common.js";
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||
|
||||
const logger = createLogger("FS");
|
||||
const logger = createLogger('FS');
|
||||
|
||||
// Re-export shared utilities
|
||||
export { getErrorMessageShared as getErrorMessage };
|
||||
|
||||
@@ -2,33 +2,35 @@
|
||||
* POST /browse endpoint - Browse directories for file browser UI
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { getAllowedRootDirectory, PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createBrowseHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { dirPath } = req.body as { dirPath?: string };
|
||||
|
||||
// Default to home directory if no path provided
|
||||
const targetPath = dirPath ? path.resolve(dirPath) : os.homedir();
|
||||
// Default to ALLOWED_ROOT_DIRECTORY if set, otherwise home directory
|
||||
const defaultPath = getAllowedRootDirectory() || os.homedir();
|
||||
const targetPath = dirPath ? path.resolve(dirPath) : defaultPath;
|
||||
|
||||
// Detect available drives on Windows
|
||||
const detectDrives = async (): Promise<string[]> => {
|
||||
if (os.platform() !== "win32") {
|
||||
if (os.platform() !== 'win32') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const drives: string[] = [];
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
for (const letter of letters) {
|
||||
const drivePath = `${letter}:\\`;
|
||||
try {
|
||||
await fs.access(drivePath);
|
||||
await secureFs.access(drivePath);
|
||||
drives.push(drivePath);
|
||||
} catch {
|
||||
// Drive doesn't exist, skip it
|
||||
@@ -46,21 +48,19 @@ export function createBrowseHandler() {
|
||||
const drives = await detectDrives();
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(targetPath);
|
||||
const stats = await secureFs.stat(targetPath);
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "Path is not a directory" });
|
||||
res.status(400).json({ success: false, error: 'Path is not a directory' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Read directory contents
|
||||
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
||||
const entries = await secureFs.readdir(targetPath, { withFileTypes: true });
|
||||
|
||||
// Filter for directories only and add parent directory option
|
||||
const directories = entries
|
||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
||||
.map((entry) => ({
|
||||
name: entry.name,
|
||||
path: path.join(targetPath, entry.name),
|
||||
@@ -76,10 +76,8 @@ export function createBrowseHandler() {
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle permission errors gracefully - still return path info so user can navigate away
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to read directory";
|
||||
const isPermissionError =
|
||||
errorMessage.includes("EPERM") || errorMessage.includes("EACCES");
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to read directory';
|
||||
const isPermissionError = errorMessage.includes('EPERM') || errorMessage.includes('EACCES');
|
||||
|
||||
if (isPermissionError) {
|
||||
// Return success with empty directories so user can still navigate to parent
|
||||
@@ -90,7 +88,7 @@ export function createBrowseHandler() {
|
||||
directories: [],
|
||||
drives,
|
||||
warning:
|
||||
"Permission denied - grant Full Disk Access to Terminal in System Preferences > Privacy & Security",
|
||||
'Permission denied - grant Full Disk Access to Terminal in System Preferences > Privacy & Security',
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
@@ -100,7 +98,13 @@ export function createBrowseHandler() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Browse directories failed");
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
logError(error, 'Browse directories failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* POST /delete-board-background endpoint - Delete board background image
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { getBoardDir } from "../../../lib/automaker-paths.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { getBoardDir } from '@automaker/platform';
|
||||
|
||||
export function createDeleteBoardBackgroundHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -16,7 +16,7 @@ export function createDeleteBoardBackgroundHandler() {
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath is required",
|
||||
error: 'projectPath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -26,10 +26,10 @@ export function createDeleteBoardBackgroundHandler() {
|
||||
|
||||
try {
|
||||
// Try to remove all background files in the board directory
|
||||
const files = await fs.readdir(boardDir);
|
||||
const files = await secureFs.readdir(boardDir);
|
||||
for (const file of files) {
|
||||
if (file.startsWith("background")) {
|
||||
await fs.unlink(path.join(boardDir, file));
|
||||
if (file.startsWith('background')) {
|
||||
await secureFs.unlink(path.join(boardDir, file));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -38,7 +38,7 @@ export function createDeleteBoardBackgroundHandler() {
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Delete board background failed");
|
||||
logError(error, 'Delete board background failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /delete endpoint - Delete file
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import { validatePath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createDeleteHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,16 +13,21 @@ export function createDeleteHandler() {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
await fs.rm(resolvedPath, { recursive: true });
|
||||
await secureFs.rm(filePath, { recursive: true });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Delete file failed");
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
logError(error, 'Delete file failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /exists endpoint - Check if file/directory exists
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createExistsHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,22 +13,28 @@ export function createExistsHandler() {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// For exists, we check but don't require the path to be pre-allowed
|
||||
// This allows the UI to validate user-entered paths
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
|
||||
try {
|
||||
await fs.access(resolvedPath);
|
||||
await secureFs.access(filePath);
|
||||
res.json({ success: true, exists: true });
|
||||
} catch {
|
||||
} catch (accessError) {
|
||||
// Check if it's a path not allowed error vs file not existing
|
||||
if (accessError instanceof PathNotAllowedError) {
|
||||
throw accessError;
|
||||
}
|
||||
res.json({ success: true, exists: false });
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Check exists failed");
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
logError(error, 'Check exists failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* GET /image endpoint - Serve image files
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createImageHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -16,7 +17,7 @@ export function createImageHandler() {
|
||||
};
|
||||
|
||||
if (!imagePath) {
|
||||
res.status(400).json({ success: false, error: "path is required" });
|
||||
res.status(400).json({ success: false, error: 'path is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,40 +25,41 @@ export function createImageHandler() {
|
||||
const fullPath = path.isAbsolute(imagePath)
|
||||
? imagePath
|
||||
: projectPath
|
||||
? path.join(projectPath, imagePath)
|
||||
: imagePath;
|
||||
? path.join(projectPath, imagePath)
|
||||
: imagePath;
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(fullPath);
|
||||
} catch {
|
||||
res.status(404).json({ success: false, error: "Image not found" });
|
||||
await secureFs.access(fullPath);
|
||||
} catch (accessError) {
|
||||
if (accessError instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: 'Path not allowed' });
|
||||
return;
|
||||
}
|
||||
res.status(404).json({ success: false, error: 'Image not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the file
|
||||
const buffer = await fs.readFile(fullPath);
|
||||
const buffer = await secureFs.readFile(fullPath);
|
||||
|
||||
// Determine MIME type from extension
|
||||
const ext = path.extname(fullPath).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
".bmp": "image/bmp",
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.bmp': 'image/bmp',
|
||||
};
|
||||
|
||||
res.setHeader(
|
||||
"Content-Type",
|
||||
mimeTypes[ext] || "application/octet-stream"
|
||||
);
|
||||
res.setHeader("Cache-Control", "public, max-age=3600");
|
||||
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
res.send(buffer);
|
||||
} catch (error) {
|
||||
logError(error, "Serve image failed");
|
||||
logError(error, 'Serve image failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* Handles symlinks safely to avoid ELOOP errors
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { addAllowedPath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createMkdirHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,7 +15,7 @@ export function createMkdirHandler() {
|
||||
const { dirPath } = req.body as { dirPath: string };
|
||||
|
||||
if (!dirPath) {
|
||||
res.status(400).json({ success: false, error: "dirPath is required" });
|
||||
res.status(400).json({ success: false, error: 'dirPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,45 +23,47 @@ export function createMkdirHandler() {
|
||||
|
||||
// Check if path already exists using lstat (doesn't follow symlinks)
|
||||
try {
|
||||
const stats = await fs.lstat(resolvedPath);
|
||||
const stats = await secureFs.lstat(resolvedPath);
|
||||
// Path exists - if it's a directory or symlink, consider it success
|
||||
if (stats.isDirectory() || stats.isSymbolicLink()) {
|
||||
addAllowedPath(resolvedPath);
|
||||
res.json({ success: true });
|
||||
return;
|
||||
}
|
||||
// It's a file - can't create directory
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Path exists and is not a directory",
|
||||
error: 'Path exists and is not a directory',
|
||||
});
|
||||
return;
|
||||
} catch (statError: any) {
|
||||
// ENOENT means path doesn't exist - we should create it
|
||||
if (statError.code !== "ENOENT") {
|
||||
if (statError.code !== 'ENOENT') {
|
||||
// Some other error (could be ELOOP in parent path)
|
||||
throw statError;
|
||||
}
|
||||
}
|
||||
|
||||
// Path doesn't exist, create it
|
||||
await fs.mkdir(resolvedPath, { recursive: true });
|
||||
|
||||
// Add the new directory to allowed paths for tracking
|
||||
addAllowedPath(resolvedPath);
|
||||
await secureFs.mkdir(resolvedPath, { recursive: true });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle ELOOP specifically
|
||||
if (error.code === "ELOOP") {
|
||||
logError(error, "Create directory failed - symlink loop detected");
|
||||
if (error.code === 'ELOOP') {
|
||||
logError(error, 'Create directory failed - symlink loop detected');
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Cannot create directory: symlink loop detected in path",
|
||||
error: 'Cannot create directory: symlink loop detected in path',
|
||||
});
|
||||
return;
|
||||
}
|
||||
logError(error, "Create directory failed");
|
||||
logError(error, 'Create directory failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,26 +2,21 @@
|
||||
* POST /read endpoint - Read file
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import { validatePath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
// Optional files that are expected to not exist in new projects
|
||||
// Don't log ENOENT errors for these to reduce noise
|
||||
const OPTIONAL_FILES = ["categories.json", "app_spec.txt"];
|
||||
const OPTIONAL_FILES = ['categories.json', 'app_spec.txt'];
|
||||
|
||||
function isOptionalFile(filePath: string): boolean {
|
||||
return OPTIONAL_FILES.some((optionalFile) => filePath.endsWith(optionalFile));
|
||||
}
|
||||
|
||||
function isENOENT(error: unknown): boolean {
|
||||
return (
|
||||
error !== null &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
error.code === "ENOENT"
|
||||
);
|
||||
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
||||
}
|
||||
|
||||
export function createReadHandler() {
|
||||
@@ -30,19 +25,24 @@ export function createReadHandler() {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
const content = await fs.readFile(resolvedPath, "utf-8");
|
||||
const content = await secureFs.readFile(filePath, 'utf-8');
|
||||
|
||||
res.json({ success: true, content });
|
||||
} catch (error) {
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't log ENOENT errors for optional files (expected to be missing in new projects)
|
||||
const shouldLog = !(isENOENT(error) && isOptionalFile(req.body?.filePath || ""));
|
||||
const shouldLog = !(isENOENT(error) && isOptionalFile(req.body?.filePath || ''));
|
||||
if (shouldLog) {
|
||||
logError(error, "Read file failed");
|
||||
logError(error, 'Read file failed');
|
||||
}
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /readdir endpoint - Read directory
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import { validatePath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createReaddirHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,12 +13,11 @@ export function createReaddirHandler() {
|
||||
const { dirPath } = req.body as { dirPath: string };
|
||||
|
||||
if (!dirPath) {
|
||||
res.status(400).json({ success: false, error: "dirPath is required" });
|
||||
res.status(400).json({ success: false, error: 'dirPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(dirPath);
|
||||
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
||||
const entries = await secureFs.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
const result = entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
@@ -28,7 +27,13 @@ export function createReaddirHandler() {
|
||||
|
||||
res.json({ success: true, entries: result });
|
||||
} catch (error) {
|
||||
logError(error, "Read directory failed");
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
logError(error, 'Read directory failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
* POST /resolve-directory endpoint - Resolve directory path from directory name
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { addAllowedPath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createResolveDirectoryHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -18,9 +17,7 @@ export function createResolveDirectoryHandler() {
|
||||
};
|
||||
|
||||
if (!directoryName) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "directoryName is required" });
|
||||
res.status(400).json({ success: false, error: 'directoryName is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,9 +25,8 @@ export function createResolveDirectoryHandler() {
|
||||
if (path.isAbsolute(directoryName) || directoryName.includes(path.sep)) {
|
||||
try {
|
||||
const resolvedPath = path.resolve(directoryName);
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
const stats = await secureFs.stat(resolvedPath);
|
||||
if (stats.isDirectory()) {
|
||||
addAllowedPath(resolvedPath);
|
||||
res.json({
|
||||
success: true,
|
||||
path: resolvedPath,
|
||||
@@ -45,17 +41,11 @@ export function createResolveDirectoryHandler() {
|
||||
// Search for directory in common locations
|
||||
const searchPaths: string[] = [
|
||||
process.cwd(), // Current working directory
|
||||
process.env.HOME || process.env.USERPROFILE || "", // User home
|
||||
path.join(
|
||||
process.env.HOME || process.env.USERPROFILE || "",
|
||||
"Documents"
|
||||
),
|
||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
|
||||
process.env.HOME || process.env.USERPROFILE || '', // User home
|
||||
path.join(process.env.HOME || process.env.USERPROFILE || '', 'Documents'),
|
||||
path.join(process.env.HOME || process.env.USERPROFILE || '', 'Desktop'),
|
||||
// Common project locations
|
||||
path.join(
|
||||
process.env.HOME || process.env.USERPROFILE || "",
|
||||
"Projects"
|
||||
),
|
||||
path.join(process.env.HOME || process.env.USERPROFILE || '', 'Projects'),
|
||||
].filter(Boolean);
|
||||
|
||||
// Also check parent of current working directory
|
||||
@@ -72,7 +62,7 @@ export function createResolveDirectoryHandler() {
|
||||
for (const searchPath of searchPaths) {
|
||||
try {
|
||||
const candidatePath = path.join(searchPath, directoryName);
|
||||
const stats = await fs.stat(candidatePath);
|
||||
const stats = await secureFs.stat(candidatePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
// Verify it matches by checking for sample files
|
||||
@@ -80,15 +70,15 @@ export function createResolveDirectoryHandler() {
|
||||
let matches = 0;
|
||||
for (const sampleFile of sampleFiles.slice(0, 5)) {
|
||||
// Remove directory name prefix from sample file path
|
||||
const relativeFile = sampleFile.startsWith(directoryName + "/")
|
||||
const relativeFile = sampleFile.startsWith(directoryName + '/')
|
||||
? sampleFile.substring(directoryName.length + 1)
|
||||
: sampleFile.split("/").slice(1).join("/") ||
|
||||
sampleFile.split("/").pop() ||
|
||||
: sampleFile.split('/').slice(1).join('/') ||
|
||||
sampleFile.split('/').pop() ||
|
||||
sampleFile;
|
||||
|
||||
try {
|
||||
const filePath = path.join(candidatePath, relativeFile);
|
||||
await fs.access(filePath);
|
||||
await secureFs.access(filePath);
|
||||
matches++;
|
||||
} catch {
|
||||
// File doesn't exist, continue checking
|
||||
@@ -102,7 +92,6 @@ export function createResolveDirectoryHandler() {
|
||||
}
|
||||
|
||||
// Found matching directory
|
||||
addAllowedPath(candidatePath);
|
||||
res.json({
|
||||
success: true,
|
||||
path: candidatePath,
|
||||
@@ -121,7 +110,7 @@ export function createResolveDirectoryHandler() {
|
||||
error: `Directory "${directoryName}" not found in common locations. Please ensure the directory exists.`,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Resolve directory failed");
|
||||
logError(error, 'Resolve directory failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
* POST /save-board-background endpoint - Save board background image
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { addAllowedPath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { getBoardDir } from "../../../lib/automaker-paths.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { getBoardDir } from '@automaker/platform';
|
||||
|
||||
export function createSaveBoardBackgroundHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -22,34 +21,31 @@ export function createSaveBoardBackgroundHandler() {
|
||||
if (!data || !filename || !projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "data, filename, and projectPath are required",
|
||||
error: 'data, filename, and projectPath are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get board directory
|
||||
const boardDir = getBoardDir(projectPath);
|
||||
await fs.mkdir(boardDir, { recursive: true });
|
||||
await secureFs.mkdir(boardDir, { recursive: true });
|
||||
|
||||
// Decode base64 data (remove data URL prefix if present)
|
||||
const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
|
||||
const buffer = Buffer.from(base64Data, "base64");
|
||||
const base64Data = data.replace(/^data:image\/\w+;base64,/, '');
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// Use a fixed filename for the board background (overwrite previous)
|
||||
const ext = path.extname(filename) || ".png";
|
||||
const ext = path.extname(filename) || '.png';
|
||||
const uniqueFilename = `background${ext}`;
|
||||
const filePath = path.join(boardDir, uniqueFilename);
|
||||
|
||||
// Write file
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
// Add board directory to allowed paths
|
||||
addAllowedPath(boardDir);
|
||||
await secureFs.writeFile(filePath, buffer);
|
||||
|
||||
// Return the absolute path
|
||||
res.json({ success: true, path: filePath });
|
||||
} catch (error) {
|
||||
logError(error, "Save board background failed");
|
||||
logError(error, 'Save board background failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
* POST /save-image endpoint - Save image to .automaker images directory
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { addAllowedPath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { getImagesDir } from "../../../lib/automaker-paths.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { getImagesDir } from '@automaker/platform';
|
||||
|
||||
export function createSaveImageHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -22,36 +21,33 @@ export function createSaveImageHandler() {
|
||||
if (!data || !filename || !projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "data, filename, and projectPath are required",
|
||||
error: 'data, filename, and projectPath are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get images directory
|
||||
const imagesDir = getImagesDir(projectPath);
|
||||
await fs.mkdir(imagesDir, { recursive: true });
|
||||
await secureFs.mkdir(imagesDir, { recursive: true });
|
||||
|
||||
// Decode base64 data (remove data URL prefix if present)
|
||||
const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
|
||||
const buffer = Buffer.from(base64Data, "base64");
|
||||
const base64Data = data.replace(/^data:image\/\w+;base64,/, '');
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// Generate unique filename with timestamp
|
||||
const timestamp = Date.now();
|
||||
const ext = path.extname(filename) || ".png";
|
||||
const ext = path.extname(filename) || '.png';
|
||||
const baseName = path.basename(filename, ext);
|
||||
const uniqueFilename = `${baseName}-${timestamp}${ext}`;
|
||||
const filePath = path.join(imagesDir, uniqueFilename);
|
||||
|
||||
// Write file
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
// Add automaker directory to allowed paths
|
||||
addAllowedPath(imagesDir);
|
||||
await secureFs.writeFile(filePath, buffer);
|
||||
|
||||
// Return the absolute path
|
||||
res.json({ success: true, path: filePath });
|
||||
} catch (error) {
|
||||
logError(error, "Save image failed");
|
||||
logError(error, 'Save image failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /stat endpoint - Get file stats
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import { validatePath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStatHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,12 +13,11 @@ export function createStatHandler() {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
const stats = await secureFs.stat(filePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -30,7 +29,13 @@ export function createStatHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get file stats failed");
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
logError(error, 'Get file stats failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* POST /validate-path endpoint - Validate and add path to allowed list
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { addAllowedPath, isPathAllowed } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { isPathAllowed } from '@automaker/platform';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createValidatePathHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -14,7 +14,7 @@ export function createValidatePathHandler() {
|
||||
const { filePath } = req.body as { filePath: string };
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,28 +22,23 @@ export function createValidatePathHandler() {
|
||||
|
||||
// Check if path exists
|
||||
try {
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
const stats = await secureFs.stat(resolvedPath);
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "Path is not a directory" });
|
||||
res.status(400).json({ success: false, error: 'Path is not a directory' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to allowed paths
|
||||
addAllowedPath(resolvedPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
path: resolvedPath,
|
||||
isAllowed: isPathAllowed(resolvedPath),
|
||||
});
|
||||
} catch {
|
||||
res.status(400).json({ success: false, error: "Path does not exist" });
|
||||
res.status(400).json({ success: false, error: 'Path does not exist' });
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Validate path failed");
|
||||
logError(error, 'Validate path failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* POST /write endpoint - Write file
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { validatePath } from "../../../lib/security.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { mkdirSafe } from "../../../lib/fs-utils.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { PathNotAllowedError } from '@automaker/platform';
|
||||
import { mkdirSafe } from '@automaker/utils';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createWriteHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -18,19 +18,23 @@ export function createWriteHandler() {
|
||||
};
|
||||
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, error: "filePath is required" });
|
||||
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = validatePath(filePath);
|
||||
|
||||
// Ensure parent directory exists (symlink-safe)
|
||||
await mkdirSafe(path.dirname(resolvedPath));
|
||||
await fs.writeFile(resolvedPath, content, "utf-8");
|
||||
await mkdirSafe(path.dirname(path.resolve(filePath)));
|
||||
await secureFs.writeFile(filePath, content, 'utf-8');
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Write file failed");
|
||||
// Path not allowed - return 403 Forbidden
|
||||
if (error instanceof PathNotAllowedError) {
|
||||
res.status(403).json({ success: false, error: getErrorMessage(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
logError(error, 'Write file failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user