- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
293 lines
6.8 KiB
JavaScript
293 lines
6.8 KiB
JavaScript
/**
|
|
* Validation Middleware
|
|
*
|
|
* Provides validation middleware for route parameters and request bodies.
|
|
*/
|
|
|
|
// Session ID validation pattern
|
|
// 10-64 characters, alphanumeric, underscore, hyphen
|
|
// Updated to support timestamp-based session IDs like: session-1769029589191-gjqeg0i0i
|
|
const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]{10,64}$/;
|
|
|
|
// Terminal ID validation pattern
|
|
const TERMINAL_ID_PATTERN = /^term-[0-9]{13,}-[a-zA-Z0-9]{4,}$/;
|
|
|
|
/**
|
|
* Validate session ID format
|
|
* @param {string} sessionId - Session ID to validate
|
|
* @returns {boolean} True if valid
|
|
*/
|
|
function validateSessionIdFormat(sessionId) {
|
|
if (!sessionId) {
|
|
return false;
|
|
}
|
|
return SESSION_ID_PATTERN.test(sessionId);
|
|
}
|
|
|
|
/**
|
|
* Validate terminal ID format
|
|
* @param {string} terminalId - Terminal ID to validate
|
|
* @returns {boolean} True if valid
|
|
*/
|
|
function validateTerminalIdFormat(terminalId) {
|
|
if (!terminalId) {
|
|
return false;
|
|
}
|
|
return TERMINAL_ID_PATTERN.test(terminalId);
|
|
}
|
|
|
|
/**
|
|
* Middleware to validate session ID parameter
|
|
*/
|
|
function validateSessionId(req, res, next) {
|
|
const { sessionId } = req.params;
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({
|
|
error: 'Session ID is required',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (!validateSessionIdFormat(sessionId)) {
|
|
return res.status(400).json({
|
|
error: 'Invalid session ID format',
|
|
sessionId,
|
|
expected: '10-64 characters, alphanumeric, underscore, hyphen',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
// Check session exists
|
|
// Note: We access the global claudeService instance from app.locals
|
|
const claudeService = req.app.locals.claudeService;
|
|
if (!claudeService) {
|
|
console.error('[ValidationError] claudeService not found in app.locals');
|
|
return res.status(500).json({
|
|
error: 'Service configuration error',
|
|
statusCode: 500
|
|
});
|
|
}
|
|
|
|
const session = claudeService.getSession(sessionId);
|
|
|
|
if (!session) {
|
|
return res.status(404).json({
|
|
error: 'Session not found',
|
|
sessionId,
|
|
hint: 'The session may have been deleted or never existed',
|
|
statusCode: 404
|
|
});
|
|
}
|
|
|
|
// Attach session context to request
|
|
req.sessionContext = session;
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Middleware to validate terminal ID parameter
|
|
*/
|
|
function validateTerminalId(req, res, next) {
|
|
const { terminalId } = req.params;
|
|
|
|
if (!terminalId) {
|
|
return res.status(400).json({
|
|
error: 'Terminal ID is required',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (!validateTerminalIdFormat(terminalId)) {
|
|
return res.status(400).json({
|
|
error: 'Invalid terminal ID format',
|
|
terminalId,
|
|
expected: 'Format: term-{timestamp}-{random}',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
// Check terminal exists
|
|
// Note: We access the global terminalService instance from app.locals
|
|
const terminalService = req.app.locals.terminalService;
|
|
if (!terminalService) {
|
|
console.error('[ValidationError] terminalService not found in app.locals');
|
|
return res.status(500).json({
|
|
error: 'Service configuration error',
|
|
statusCode: 500
|
|
});
|
|
}
|
|
|
|
const result = terminalService.getTerminal(terminalId);
|
|
|
|
if (!result.success) {
|
|
return res.status(404).json({
|
|
error: 'Terminal not found',
|
|
terminalId,
|
|
hint: 'The terminal may have been closed',
|
|
statusCode: 404
|
|
});
|
|
}
|
|
|
|
// Attach terminal context to request
|
|
req.terminalContext = result.terminal;
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Middleware to validate command/prompt body
|
|
*/
|
|
function validateCommand(req, res, next) {
|
|
const { command } = req.body;
|
|
|
|
if (!command || typeof command !== 'string') {
|
|
return res.status(400).json({
|
|
error: 'Command is required and must be a string',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (command.trim().length === 0) {
|
|
return res.status(400).json({
|
|
error: 'Command cannot be empty',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (command.length > 100000) {
|
|
return res.status(400).json({
|
|
error: 'Command too large (max 100KB)',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Middleware to validate operations array
|
|
*/
|
|
function validateOperations(req, res, next) {
|
|
const { operations } = req.body;
|
|
|
|
if (!operations || !Array.isArray(operations)) {
|
|
return res.status(400).json({
|
|
error: 'Operations array is required',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (operations.length === 0) {
|
|
return res.status(400).json({
|
|
error: 'Operations array cannot be empty',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (operations.length > 1000) {
|
|
return res.status(400).json({
|
|
error: 'Too many operations (max 1000)',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
// Validate each operation
|
|
for (let i = 0; i < operations.length; i++) {
|
|
const op = operations[i];
|
|
|
|
if (!op.type) {
|
|
return res.status(400).json({
|
|
error: `Operation at index ${i} missing 'type' field`,
|
|
operation: op,
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
const validTypes = ['read', 'write', 'delete', 'list', 'search'];
|
|
if (!validTypes.includes(op.type)) {
|
|
return res.status(400).json({
|
|
error: `Invalid operation type at index ${i}: ${op.type}`,
|
|
validTypes,
|
|
statusCode: 400
|
|
});
|
|
}
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Middleware to validate response for operations preview
|
|
*/
|
|
function validateResponse(req, res, next) {
|
|
const { response } = req.body;
|
|
|
|
if (!response || typeof response !== 'string') {
|
|
return res.status(400).json({
|
|
error: 'Response is required and must be a string',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (response.trim().length === 0) {
|
|
return res.status(400).json({
|
|
error: 'Response cannot be empty',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
if (response.length > 10000000) {
|
|
return res.status(400).json({
|
|
error: 'Response too large (max 10MB)',
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Error handler middleware
|
|
*/
|
|
function errorHandler(err, req, res, next) {
|
|
console.error('[ErrorHandler]', err);
|
|
|
|
// Handle validation errors
|
|
if (err.name === 'ValidationError') {
|
|
return res.status(400).json({
|
|
error: 'Validation error',
|
|
details: err.message,
|
|
statusCode: 400
|
|
});
|
|
}
|
|
|
|
// Handle not found errors
|
|
if (err.name === 'NotFoundError') {
|
|
return res.status(404).json({
|
|
error: 'Resource not found',
|
|
details: err.message,
|
|
statusCode: 404
|
|
});
|
|
}
|
|
|
|
// Default error response
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: err.message,
|
|
statusCode: 500
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
validateSessionIdFormat,
|
|
validateTerminalIdFormat,
|
|
validateSessionId,
|
|
validateTerminalId,
|
|
validateCommand,
|
|
validateOperations,
|
|
validateResponse,
|
|
errorHandler
|
|
};
|