Spaces:
Running
Running
| // pages/api/mcp-handler.js - Updated for Passkey System | |
| import fs from 'fs/promises' | |
| import fssync from 'fs'; | |
| import path from 'path'; | |
| // Use /data for Hugging Face Spaces persistent storage | |
| const DATA_DIR = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'public', 'data'); | |
| const PUBLIC_DIR = path.join(DATA_DIR, 'public'); | |
| // Ensure directories exist | |
| async function ensureDirectories() { | |
| if (!fssync.existsSync(DATA_DIR)) { | |
| await fs.mkdir(DATA_DIR, { recursive: true }); | |
| } | |
| if (!fssync.existsSync(PUBLIC_DIR)) { | |
| await fs.mkdir(PUBLIC_DIR, { recursive: true }); | |
| } | |
| } | |
| // Validate passkey format (alphanumeric and hyphens/underscores only) | |
| function isValidPasskey(passkey) { | |
| return passkey && /^[a-zA-Z0-9_-]+$/.test(passkey) && passkey.length >= 4; | |
| } | |
| // Sanitize filename to prevent path traversal | |
| function sanitizeFileName(fileName) { | |
| return fileName.replace(/[^a-zA-Z0-9._-]/g, '_'); | |
| } | |
| // Sanitize passkey (used for directory names) | |
| function sanitizePasskey(passkey) { | |
| return passkey.replace(/[^a-zA-Z0-9_-]/g, ''); | |
| } | |
| export default async function handler(req, res) { | |
| // Enable CORS | |
| res.setHeader('Access-Control-Allow-Origin', '*'); | |
| res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); | |
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); | |
| if (req.method === 'OPTIONS') { | |
| return res.status(200).end(); | |
| } | |
| try { | |
| await ensureDirectories(); | |
| if (req.method === 'POST') { | |
| const { passkey, action, fileName, content, isPublic = false } = req.body; | |
| // Public files don't need passkey | |
| if (!isPublic && !isValidPasskey(passkey)) { | |
| return res.status(400).json({ | |
| success: false, | |
| error: 'Invalid or missing passkey (minimum 4 characters required)' | |
| }); | |
| } | |
| // Validate required fields | |
| if (!action || !fileName || content === undefined) { | |
| return res.status(400).json({ | |
| success: false, | |
| error: 'Missing required fields: action, fileName, or content' | |
| }); | |
| } | |
| const sanitizedFileName = sanitizeFileName(fileName); | |
| try { | |
| let targetDir; | |
| let responseData = {}; | |
| if (isPublic) { | |
| targetDir = PUBLIC_DIR; | |
| responseData.isPublic = true; | |
| responseData.location = 'Public Files'; | |
| } else { | |
| const sanitizedKey = sanitizePasskey(passkey); | |
| targetDir = path.join(DATA_DIR, sanitizedKey); | |
| if (!fssync.existsSync(targetDir)) { | |
| await fs.mkdir(targetDir, { recursive: true }); | |
| } | |
| responseData.passkey = sanitizedKey; | |
| responseData.location = 'Secure Data'; | |
| } | |
| // Handle different actions | |
| switch (action) { | |
| case 'save_file': | |
| case 'deploy_quiz': | |
| case 'save': { | |
| const filePath = path.join(targetDir, sanitizedFileName); | |
| await fs.writeFile(filePath, content, 'utf8'); | |
| return res.status(200).json({ | |
| success: true, | |
| message: `File saved successfully to ${responseData.location}`, | |
| fileName: sanitizedFileName, | |
| action: action, | |
| ...responseData, | |
| url: isPublic | |
| ? `${process.env.SPACE_ID ? 'https://mcp-1st-birthday-reuben-os.hf.space' : 'http://localhost:3000'}/data/public/${sanitizedFileName}` | |
| : null | |
| }); | |
| } | |
| case 'delete_file': | |
| case 'delete': { | |
| const filePath = path.join(targetDir, sanitizedFileName); | |
| try { | |
| await fs.unlink(filePath); | |
| return res.status(200).json({ | |
| success: true, | |
| message: `File deleted successfully`, | |
| fileName: sanitizedFileName, | |
| ...responseData | |
| }); | |
| } catch (err) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: 'File not found' | |
| }); | |
| } | |
| } | |
| case 'clear_session': | |
| case 'clear': { | |
| if (isPublic) { | |
| return res.status(400).json({ | |
| success: false, | |
| error: 'Cannot clear public folder via this endpoint' | |
| }); | |
| } | |
| const files = await fs.readdir(targetDir); | |
| let deletedCount = 0; | |
| for (const file of files) { | |
| await fs.unlink(path.join(targetDir, file)); | |
| deletedCount++; | |
| } | |
| return res.status(200).json({ | |
| success: true, | |
| message: `Cleared ${deletedCount} files`, | |
| deletedCount, | |
| ...responseData | |
| }); | |
| } | |
| default: | |
| return res.status(400).json({ | |
| success: false, | |
| error: `Unknown action: ${action}` | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('File operation error:', error); | |
| return res.status(500).json({ | |
| success: false, | |
| error: `Failed to ${action}: ${error.message}` | |
| }); | |
| } | |
| } else if (req.method === 'GET') { | |
| const { passkey, isPublic } = req.query; | |
| let targetDir; | |
| let responseData = {}; | |
| if (isPublic === 'true') { | |
| targetDir = PUBLIC_DIR; | |
| responseData.isPublic = true; | |
| responseData.location = 'Public Files'; | |
| } else { | |
| if (!isValidPasskey(passkey)) { | |
| return res.status(400).json({ | |
| success: false, | |
| error: 'Invalid or missing passkey' | |
| }); | |
| } | |
| const sanitizedKey = sanitizePasskey(passkey); | |
| targetDir = path.join(DATA_DIR, sanitizedKey); | |
| if (!fssync.existsSync(targetDir)) { | |
| return res.status(200).json({ | |
| success: true, | |
| passkey: sanitizedKey, | |
| files: [], | |
| count: 0, | |
| message: 'No files found for this passkey' | |
| }); | |
| } | |
| responseData.passkey = sanitizedKey; | |
| responseData.location = 'Secure Data'; | |
| } | |
| try { | |
| const allFiles = await fs.readdir(targetDir); | |
| const fileList = await Promise.all( | |
| allFiles.map(async (file) => { | |
| const filePath = path.join(targetDir, file); | |
| const stats = await fs.stat(filePath); | |
| // Read file content (with size limit) | |
| let content = null; | |
| if (stats.size < 1024 * 1024) { // 1MB limit | |
| try { | |
| content = await fs.readFile(filePath, 'utf8'); | |
| } catch (err) { | |
| console.error(`Error reading file ${file}:`, err); | |
| } | |
| } | |
| return { | |
| name: file, | |
| size: stats.size, | |
| modified: stats.mtime, | |
| isQuiz: file === 'quiz.json', | |
| content: content, | |
| extension: path.extname(file).substring(1) | |
| }; | |
| }) | |
| ); | |
| // Sort by modification time (newest first) | |
| fileList.sort((a, b) => new Date(b.modified) - new Date(a.modified)); | |
| return res.status(200).json({ | |
| success: true, | |
| files: fileList, | |
| count: fileList.length, | |
| ...responseData | |
| }); | |
| } catch (error) { | |
| console.error('Error reading files:', error); | |
| return res.status(500).json({ | |
| success: false, | |
| error: `Failed to retrieve files: ${error.message}` | |
| }); | |
| } | |
| } else { | |
| return res.status(405).json({ | |
| success: false, | |
| error: 'Method not allowed' | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Unexpected error:', error); | |
| return res.status(500).json({ | |
| success: false, | |
| error: `Server error: ${error.message}` | |
| }); | |
| } | |
| } |