const express = require('express'); const Database = require('better-sqlite3'); const cors = require('cors'); const fs = require('fs'); const app = express(); // Parse ALLOWED_ORIGINS from Hugging Face secrets (environment variables) let allowedOrigins = '*'; // Default fallback to allow all if secret is not set if (process.env.ALLOWED_ORIGINS) { // Splits a comma-separated string like "https://example.com,https://app.example.com" allowedOrigins = process.env.ALLOWED_ORIGINS.split(',').map(url => url.trim()); console.log(`šŸ”’ CORS Restricted to Origins:`, allowedOrigins); } else { console.log(`āš ļø CORS is globally accessible (*). Set ALLOWED_ORIGINS secret to secure.`); } // CORS Middleware Configuration app.use(cors({ origin: allowedOrigins, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'X-User-ID', 'X-User-Email'] })); app.use(express.json({ limit: '50mb' })); // ============================================ // LOGGING MIDDLEWARE // ============================================ const logRequest = (req, res, next) => { const timestamp = new Date().toISOString(); const userId = req.headers['x-user-id'] || 'NO_USER_ID'; const userEmail = req.headers['x-user-email'] || ''; console.log(`\nšŸ“„ [${timestamp}] ${req.method} ${req.path}`); console.log(` šŸ‘¤ User ID: ${userId}${userEmail ? ` (${userEmail})` : ''}`); console.log(` šŸ”— Origin: ${req.headers.origin || 'Unknown'}`); console.log(` šŸ“ IP: ${req.ip}`); if (req.method === 'POST' || req.method === 'PATCH') { console.log(` šŸ“¦ Request Body:`, JSON.stringify(req.body, null, 2).substring(0, 500)); } if (req.query && Object.keys(req.query).length > 0) { console.log(` šŸ” Query Params:`, req.query); } // Store original send function const originalSend = res.send; res.send = function(body) { const responseTime = Date.now() - req.startTime; console.log(`\nšŸ“¤ [${timestamp}] ${req.method} ${req.path} → ${res.statusCode}`); console.log(` ā±ļø Response Time: ${responseTime}ms`); if (res.statusCode >= 400) { console.log(` āŒ Error Response:`, body); } else { if (typeof body === 'string') { try { const parsed = JSON.parse(body); console.log(` āœ… Success Response:`, JSON.stringify(parsed, null, 2).substring(0, 300)); } catch { console.log(` āœ… Success Response: ${body.substring(0, 100)}...`); } } else if (typeof body === 'object') { console.log(` āœ… Success Response:`, JSON.stringify(body, null, 2).substring(0, 300)); } } originalSend.call(this, body); }; req.startTime = Date.now(); next(); }; app.use(logRequest); // ============================================ // DATABASE INITIALIZATION // ============================================ const path = require('path'); let databaseDir; if (fs.existsSync('/data')) { databaseDir = '/data'; console.log("āœ… PRODUCTION MODE: Using Persistent Storage at /data"); } else { databaseDir = path.join(process.cwd(), "data"); if (!fs.existsSync(databaseDir)) { fs.mkdirSync(databaseDir, { recursive: true }); } console.log(`āš ļø LOCAL MODE: Using local storage at ${databaseDir}`); } // Initialize Database const dbPath = path.join(databaseDir, 'chat_storage.db'); console.log(`šŸ’¾ Initializing database at: ${dbPath}`); try { const db = new Database(dbPath); console.log('āœ… Database connection established'); // Database settings db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); console.log('šŸ”§ Database pragmas set'); // Schema const initSchema = () => { const schema = ` CREATE TABLE IF NOT EXISTS chats ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, title TEXT NOT NULL, model TEXT DEFAULT 'default', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, chat_id INTEGER NOT NULL, role TEXT CHECK( role IN ('user','assistant') ) NOT NULL, content TEXT NOT NULL, model TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (chat_id) REFERENCES chats (id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS chat_workspaces ( chat_id INTEGER PRIMARY KEY, content TEXT NOT NULL DEFAULT '', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (chat_id) REFERENCES chats (id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_chats_user ON chats(user_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages(chat_id); CREATE INDEX IF NOT EXISTS idx_chat_workspaces_updated ON chat_workspaces(updated_at DESC); /* ── Ant-Editor Document Storage ── */ CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, title TEXT NOT NULL DEFAULT 'Untitled Document', content TEXT NOT NULL DEFAULT '', language TEXT NOT NULL DEFAULT 'en', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_documents_user ON documents(user_id, updated_at DESC); /* ── User Identity Tracking ── */ /* Maps user_id (UUID from Cloudflare KV) to email for traceability. */ /* The proxy (catchall.js) verifies the session and sends both headers. */ CREATE TABLE IF NOT EXISTS users ( user_id TEXT PRIMARY KEY, email TEXT NOT NULL DEFAULT '', first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); /* ── Document Sharing ── */ /* Maps a random share_token (UUID) to a document ID. */ /* Anyone with the token can read/write the document — no auth. */ /* ON DELETE CASCADE: deleting the source document nukes shares. */ CREATE TABLE IF NOT EXISTS shared_documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, share_token TEXT NOT NULL UNIQUE, document_id INTEGER NOT NULL, created_by TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS idx_shared_token ON shared_documents(share_token); CREATE INDEX IF NOT EXISTS idx_shared_doc_id ON shared_documents(document_id); `; db.exec(schema); console.log('āœ… Database schema initialized'); // Log existing data counts const chatCount = db.prepare('SELECT COUNT(*) as count FROM chats').get().count; const messageCount = db.prepare('SELECT COUNT(*) as count FROM messages').get().count; console.log(`šŸ“Š Database Stats: ${chatCount} chats, ${messageCount} messages`); }; initSchema(); // ============================================ // HELPER FUNCTIONS // ============================================ // Middleware to get user ID + canonical email-based identity resolution. // // Problem: The same Google account gets a DIFFERENT random UUID in each // Cloudflare environment (dev vs prod) because each has its own KV store. // But they all share this single backend. Without resolution, the same // person's documents are split across multiple user_ids. // // Solution: The `users` table tracks the FIRST user_id we ever saw for // a given email. On subsequent requests, if the email arrives with a // DIFFERENT user_id (from another environment), we: // 1. Auto-migrate their documents and chats to the canonical user_id // 2. Override req.userId to the canonical one // // This ensures one email = one identity = one set of documents, always. const requireUser = (req, res, next) => { // Accept user ID from header OR query param (for sendBeacon) const incomingUserId = req.headers['x-user-id'] || req.query.user_id; if (!incomingUserId || typeof incomingUserId !== 'string') { console.log(`āŒ Missing or invalid X-User-ID header`); return res.status(401).json({ error: 'User identity required' }); } // Read verified email from proxy (if session was valid) const userEmail = req.headers['x-user-email'] || ''; req.userEmail = userEmail; // ── Canonical user resolution ── // If we have an email, check if this email already has a canonical user_id let canonicalUserId = incomingUserId; if (userEmail) { try { // Look up the FIRST user_id ever registered for this email const existing = db.prepare( `SELECT user_id FROM users WHERE email = ? ORDER BY first_seen_at ASC LIMIT 1` ).get(userEmail); if (existing && existing.user_id !== incomingUserId) { // This email already has a canonical user_id from a different environment. // Use the canonical one and migrate any orphaned data. canonicalUserId = existing.user_id; console.log( `šŸ”„ [identity] Email ${userEmail} → canonical user ${canonicalUserId.substring(0, 8)}… ` + `(incoming ${incomingUserId.substring(0, 8)}… will be merged)` ); // Auto-migrate documents from the incoming (non-canonical) user_id const docMigration = db.prepare( `UPDATE documents SET user_id = ? WHERE user_id = ?` ).run(canonicalUserId, incomingUserId); if (docMigration.changes > 0) { console.log(` šŸ“„ Migrated ${docMigration.changes} document(s) → ${canonicalUserId.substring(0, 8)}…`); } // Auto-migrate chats from the incoming user_id const chatMigration = db.prepare( `UPDATE chats SET user_id = ? WHERE user_id = ?` ).run(canonicalUserId, incomingUserId); if (chatMigration.changes > 0) { console.log(` šŸ’¬ Migrated ${chatMigration.changes} chat(s) → ${canonicalUserId.substring(0, 8)}…`); } // Clean up the non-canonical user record if it exists db.prepare(`DELETE FROM users WHERE user_id = ? AND email = ?`) .run(incomingUserId, userEmail); } else if (!existing) { // First time this email is seen — register the canonical mapping db.prepare(` INSERT INTO users (user_id, email, first_seen_at, last_seen_at) VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `).run(incomingUserId, userEmail); } else { // Same user_id as canonical — just update last_seen db.prepare(` UPDATE users SET last_seen_at = CURRENT_TIMESTAMP WHERE user_id = ? `).run(canonicalUserId); } } catch (err) { // Non-fatal: don't block the request if identity resolution fails console.warn(`āš ļø Identity resolution failed:`, err.message); } } req.userId = canonicalUserId; console.log(`āœ… User authenticated: ${canonicalUserId.substring(0, 10)}...${userEmail ? ` (${userEmail})` : ' (guest)'}`); next(); }; app.get('/health', (req, res) => { console.log('ā¤ļø Health check requested'); try { // Test database connection db.prepare('SELECT 1 as test').get(); res.json({ status: 'active', database: 'connected', timestamp: new Date().toISOString(), uptime: process.uptime() }); } catch (error) { console.error('āŒ Health check failed:', error); res.status(500).json({ status: 'degraded', database: 'disconnected', error: error.message }); } }); // Debug stats endpoint app.get('/debug/stats', (req, res) => { try { console.log('šŸ“Š Debug stats requested'); // Get user count const userCountStmt = db.prepare(` SELECT COUNT(DISTINCT user_id) as user_count FROM chats `); const userCount = userCountStmt.get().user_count; // Get chat count by user const chatsByUserStmt = db.prepare(` SELECT user_id, COUNT(*) as chat_count FROM chats GROUP BY user_id ORDER BY chat_count DESC LIMIT 10 `); const topUsers = chatsByUserStmt.all(); // Get message count by chat const messagesByChatStmt = db.prepare(` SELECT chat_id, COUNT(*) as message_count FROM messages GROUP BY chat_id ORDER BY message_count DESC LIMIT 10 `); const topChats = messagesByChatStmt.all(); // Get recent activity const recentChatsStmt = db.prepare(` SELECT id, user_id, title, updated_at FROM chats ORDER BY updated_at DESC LIMIT 5 `); const recentChats = recentChatsStmt.all(); res.json({ database: dbPath, users: { total: userCount, top: topUsers }, chats: { total: db.prepare('SELECT COUNT(*) as count FROM chats').get().count, recent: recentChats }, messages: { total: db.prepare('SELECT COUNT(*) as count FROM messages').get().count, top_chats: topChats }, chat_workspaces: { total: db.prepare('SELECT COUNT(*) as count FROM chat_workspaces').get().count }, documents: { total: db.prepare('SELECT COUNT(*) as count FROM documents').get().count }, timestamp: new Date().toISOString() }); } catch (error) { console.error('āŒ Debug stats error:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/chats', requireUser, (req, res) => { try { console.log(`šŸ“‹ Fetching chats for user: ${req.userId.substring(0, 10)}...`); const limit = Math.min(parseInt(req.query.limit) || 50, 100); const offset = parseInt(req.query.offset) || 0; const stmt = db.prepare(` SELECT id, title, model, created_at, updated_at FROM chats WHERE user_id = ? ORDER BY updated_at DESC LIMIT ? OFFSET ? `); const chats = stmt.all(req.userId, limit, offset); console.log(`āœ… Found ${chats.length} chats for user`); res.json(chats); } catch (err) { console.error(`āŒ Error fetching chats for user ${req.userId}:`, err); res.status(500).json({ error: err.message }); } }); app.post('/api/chats', requireUser, (req, res) => { try { const { title, model } = req.body; console.log(`šŸ“ Creating new chat for user ${req.userId.substring(0, 10)}...`); console.log(` Title: ${title}, Model: ${model || 'default'}`); if (!title || typeof title !== 'string') { console.log(`āŒ Invalid title: ${title}`); return res.status(400).json({ error: 'Valid title required' }); } const stmt = db.prepare(` INSERT INTO chats (user_id, title, model) VALUES (?, ?, ?) `); const info = stmt.run(req.userId, title.trim(), model || 'default'); console.log(`āœ… Chat created with ID: ${info.lastInsertRowid}`); const newChat = db.prepare(` SELECT * FROM chats WHERE id = ? `).get(info.lastInsertRowid); console.log(`šŸ“Š New chat details:`, newChat); res.status(201).json(newChat); } catch (err) { console.error(`āŒ Error creating chat for user ${req.userId}:`, err); res.status(500).json({ error: err.message }); } }); app.get('/api/chats/:id/messages', requireUser, (req, res) => { try { const chatId = req.params.id; console.log(`šŸ“© Fetching messages for chat ${chatId} (user: ${req.userId.substring(0, 10)}...)`); // Verify ownership const chat = db.prepare(` SELECT id, title, user_id FROM chats WHERE id = ? AND user_id = ? `).get(chatId, req.userId); if (!chat) { console.log(`āŒ Chat ${chatId} not found or not owned by user ${req.userId}`); return res.status(404).json({ error: 'Chat not found' }); } console.log(`āœ… Chat found: "${chat.title}" (ID: ${chat.id})`); const messages = db.prepare(` SELECT * FROM messages WHERE chat_id = ? ORDER BY timestamp ASC `).all(chatId); console.log(`āœ… Found ${messages.length} messages for chat ${chatId}`); // Log first few messages if (messages.length > 0) { console.log(` Sample messages:`); messages.slice(0, 3).forEach(msg => { console.log(` - ${msg.role}: ${msg.content.substring(0, 50)}...`); }); if (messages.length > 3) { console.log(` ... and ${messages.length - 3} more`); } } res.json(messages); } catch (err) { console.error(`āŒ Error fetching messages for chat ${req.params.id}:`, err); res.status(500).json({ error: err.message }); } }); app.post('/api/messages', requireUser, (req, res) => { try { const { chatId, role, content, model } = req.body; console.log(`šŸ’¾ Saving message to chat ${chatId}`); console.log(` Role: ${role}, Model: ${model || 'none'}`); console.log(` Content preview: ${content.substring(0, 100)}...`); if (!chatId || !role || !content) { console.log(`āŒ Invalid message data: chatId=${chatId}, role=${role}, content=${content ? 'present' : 'missing'}`); return res.status(400).json({ error: 'Invalid message data' }); } // Verify ownership const chat = db.prepare(` SELECT id, title, user_id FROM chats WHERE id = ? AND user_id = ? `).get(chatId, req.userId); if (!chat) { console.log(`āŒ Chat ${chatId} not found or not owned by user ${req.userId}`); return res.status(404).json({ error: 'Chat not found' }); } console.log(`āœ… Chat verified: "${chat.title}"`); // Insert message const insertStmt = db.prepare(` INSERT INTO messages (chat_id, role, content, model) VALUES (?, ?, ?, ?) `); const info = insertStmt.run(chatId, role, content, model || null); console.log(`āœ… Message saved with ID: ${info.lastInsertRowid}`); // Update chat timestamp db.prepare(` UPDATE chats SET updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(chatId); console.log(`šŸ•’ Updated chat ${chatId} timestamp`); // Return the saved message const newMessage = db.prepare(` SELECT * FROM messages WHERE id = ? `).get(info.lastInsertRowid); console.log(`šŸ“¤ Returning saved message with ID: ${newMessage.id}`); res.status(201).json(newMessage); } catch (err) { console.error(`āŒ Error saving message:`, err); res.status(500).json({ error: err.message }); } }); app.get('/api/chats/:id', requireUser, (req, res) => { try { const chatId = req.params.id; console.log(`šŸ” Fetching chat ${chatId} details for user ${req.userId.substring(0, 10)}...`); const chat = db.prepare(` SELECT * FROM chats WHERE id = ? AND user_id = ? `).get(chatId, req.userId); if (!chat) { console.log(`āŒ Chat ${chatId} not found`); return res.status(404).json({ error: 'Chat not found' }); } console.log(`āœ… Chat found:`, chat); res.json(chat); } catch (err) { console.error(`āŒ Error fetching chat ${req.params.id}:`, err); res.status(500).json({ error: err.message }); } }); app.delete('/api/chats/:id', requireUser, (req, res) => { try { const chatId = req.params.id; console.log(`šŸ—‘ļø Deleting chat ${chatId} for user ${req.userId.substring(0, 10)}...`); // First, get chat info for logging const chat = db.prepare(` SELECT title FROM chats WHERE id = ? AND user_id = ? `).get(chatId, req.userId); if (!chat) { console.log(`āŒ Chat ${chatId} not found or not owned by user`); return res.status(404).json({ error: 'Chat not found' }); } console.log(` Deleting chat: "${chat.title}"`); // Count messages before deletion const messageCount = db.prepare(` SELECT COUNT(*) as count FROM messages WHERE chat_id = ? `).get(chatId).count; console.log(` This will delete ${messageCount} messages`); const stmt = db.prepare(` DELETE FROM chats WHERE id = ? AND user_id = ? `); const info = stmt.run(req.params.id, req.userId); if (info.changes === 0) { console.log(`āŒ No rows deleted`); return res.status(404).json({ error: 'Chat not found' }); } console.log(`āœ… Deleted chat ${chatId} and ${messageCount} messages`); res.json({ success: true, chatId: chatId, messagesDeleted: messageCount }); } catch (err) { console.error(`āŒ Error deleting chat ${req.params.id}:`, err); res.status(500).json({ error: err.message }); } }); app.patch('/api/chats/:id', requireUser, (req, res) => { try { const { title } = req.body; const chatId = req.params.id; console.log(`āœļø Renaming chat ${chatId} for user ${req.userId.substring(0, 10)}...`); console.log(` New title: ${title}`); if (!title || typeof title !== 'string') { console.log(`āŒ Invalid title: ${title}`); return res.status(400).json({ error: 'Valid title required' }); } // Get old title for logging const oldChat = db.prepare(` SELECT title FROM chats WHERE id = ? AND user_id = ? `).get(chatId, req.userId); if (!oldChat) { console.log(`āŒ Chat ${chatId} not found`); return res.status(404).json({ error: 'Chat not found' }); } console.log(` Old title: "${oldChat.title}"`); const stmt = db.prepare(` UPDATE chats SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `); const info = stmt.run(title.trim(), req.params.id, req.userId); if (info.changes === 0) { console.log(`āŒ No rows updated`); return res.status(404).json({ error: 'Chat not found' }); } console.log(`āœ… Chat renamed from "${oldChat.title}" to "${title}"`); res.json({ success: true, oldTitle: oldChat.title, newTitle: title }); } catch (err) { console.error(`āŒ Error renaming chat ${req.params.id}:`, err); res.status(500).json({ error: err.message }); } }); app.get('/api/chats/:id/workspace', requireUser, (req, res) => { try { const chatId = req.params.id; const chat = db.prepare(` SELECT id, title FROM chats WHERE id = ? AND user_id = ? `).get(chatId, req.userId); if (!chat) { return res.status(404).json({ error: 'Chat not found' }); } const workspace = db.prepare(` SELECT chat_id, content, updated_at FROM chat_workspaces WHERE chat_id = ? `).get(chatId); res.json({ chat_id: chatId, content: workspace?.content ?? '', updated_at: workspace?.updated_at ?? null }); } catch (err) { console.error(`āŒ Error fetching workspace for chat ${req.params.id}:`, err); res.status(500).json({ error: err.message }); } }); app.put('/api/chats/:id/workspace', requireUser, (req, res) => { try { const chatId = req.params.id; const { content } = req.body; if (typeof content !== 'string') { return res.status(400).json({ error: 'Workspace content must be a string' }); } if (content.length > 5_000_000) { return res.status(413).json({ error: 'Workspace content too large' }); } const chat = db.prepare(` SELECT id, title FROM chats WHERE id = ? AND user_id = ? `).get(chatId, req.userId); if (!chat) { return res.status(404).json({ error: 'Chat not found' }); } db.prepare(` INSERT INTO chat_workspaces (chat_id, content, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(chat_id) DO UPDATE SET content = excluded.content, updated_at = CURRENT_TIMESTAMP `).run(chatId, content); const updated = db.prepare(` SELECT chat_id, content, updated_at FROM chat_workspaces WHERE chat_id = ? `).get(chatId); res.json(updated); } catch (err) { console.error(`āŒ Error saving workspace for chat ${req.params.id}:`, err); res.status(500).json({ error: err.message }); } }); // ============================================ // ANT-EDITOR DOCUMENT ENDPOINTS // These are completely separate from the chat system. // ============================================ // List all documents for a user app.get('/api/documents', requireUser, (req, res) => { try { const limit = Math.min(parseInt(req.query.limit) || 50, 200); const offset = Math.max(parseInt(req.query.offset) || 0, 0); const stmt = db.prepare(` SELECT id, title, language, created_at, updated_at, LENGTH(content) as content_length FROM documents WHERE user_id = ? ORDER BY updated_at DESC LIMIT ? OFFSET ? `); const documents = stmt.all(req.userId, limit, offset); const countStmt = db.prepare('SELECT COUNT(*) as total FROM documents WHERE user_id = ?'); const { total } = countStmt.get(req.userId); res.json({ documents, total, limit, offset }); } catch (err) { res.status(500).json({ error: err.message }); } }); // List all active shared links for the current user (with document titles) // IMPORTANT: Must be registered BEFORE /api/documents/:id to prevent // Express from matching 'shared-links' as the :id parameter. app.get('/api/documents/shared-links', requireUser, (req, res) => { try { const stmt = db.prepare(` SELECT sd.share_token, sd.document_id, sd.created_at, d.title AS document_title FROM shared_documents sd JOIN documents d ON d.id = sd.document_id WHERE sd.created_by = ? ORDER BY sd.created_at DESC `); const links = stmt.all(req.userId); console.log(`šŸ”— Listed ${links.length} shared link(s) for user ${req.userId.substring(0, 8)}…`); res.json({ shared_links: links }); } catch (err) { console.error('Failed to list shared links:', err); res.status(500).json({ error: err.message }); } }); // Get a single document (with full content) app.get('/api/documents/:id', requireUser, (req, res) => { try { const doc = db.prepare(` SELECT * FROM documents WHERE id = ? AND user_id = ? `).get(req.params.id, req.userId); if (!doc) { return res.status(404).json({ error: 'Document not found' }); } res.json(doc); } catch (err) { res.status(500).json({ error: err.message }); } }); // Create a new document app.post('/api/documents', requireUser, (req, res) => { try { const { title, content, language } = req.body; const docTitle = (title && typeof title === 'string') ? title.trim() : 'Untitled Document'; const docContent = (content && typeof content === 'string') ? content : ''; const docLanguage = (language && typeof language === 'string') ? language.trim().substring(0, 2) : 'en'; const stmt = db.prepare(` INSERT INTO documents (user_id, title, content, language) VALUES (?, ?, ?, ?) `); const info = stmt.run(req.userId, docTitle, docContent, docLanguage); const newDoc = db.prepare('SELECT * FROM documents WHERE id = ?').get(info.lastInsertRowid); res.status(201).json(newDoc); } catch (err) { res.status(500).json({ error: err.message }); } }); // Update a document (full save — title + content) app.put('/api/documents/:id', requireUser, (req, res) => { try { const { title, content, language } = req.body; // Verify ownership const existing = db.prepare(` SELECT id FROM documents WHERE id = ? AND user_id = ? `).get(req.params.id, req.userId); if (!existing) { return res.status(404).json({ error: 'Document not found' }); } const updates = []; const params = []; if (title !== undefined) { updates.push('title = ?'); params.push(typeof title === 'string' ? title.trim() : 'Untitled Document'); } if (content !== undefined) { updates.push('content = ?'); params.push(typeof content === 'string' ? content : ''); } if (language !== undefined) { updates.push('language = ?'); params.push(typeof language === 'string' ? language.trim().substring(0, 2) : 'en'); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update' }); } updates.push('updated_at = CURRENT_TIMESTAMP'); params.push(req.params.id, req.userId); db.prepare(` UPDATE documents SET ${updates.join(', ')} WHERE id = ? AND user_id = ? `).run(...params); const updatedDoc = db.prepare('SELECT * FROM documents WHERE id = ?').get(req.params.id); res.json(updatedDoc); } catch (err) { res.status(500).json({ error: err.message }); } }); // Rename a document (title only) app.patch('/api/documents/:id', requireUser, (req, res) => { try { const { title } = req.body; if (!title || typeof title !== 'string') { return res.status(400).json({ error: 'Valid title required' }); } const stmt = db.prepare(` UPDATE documents SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `); const info = stmt.run(title.trim(), req.params.id, req.userId); if (info.changes === 0) { return res.status(404).json({ error: 'Document not found' }); } res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Delete a document app.delete('/api/documents/:id', requireUser, (req, res) => { try { const stmt = db.prepare(` DELETE FROM documents WHERE id = ? AND user_id = ? `); const info = stmt.run(req.params.id, req.userId); if (info.changes === 0) { return res.status(404).json({ error: 'Document not found' }); } res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ============================================ // DOCUMENT SHARING ENDPOINTS // Share a document via a randomly generated token. // Create/delete share requires auth. Access via token is public. // ============================================ const crypto = require('crypto'); // Create (or return existing) share link for a document app.post('/api/documents/:id/share', requireUser, (req, res) => { try { const docId = req.params.id; // Verify ownership const doc = db.prepare( 'SELECT id, title FROM documents WHERE id = ? AND user_id = ?' ).get(docId, req.userId); if (!doc) { return res.status(404).json({ error: 'Document not found' }); } // Check if a share link already exists for this doc const existing = db.prepare( 'SELECT share_token FROM shared_documents WHERE document_id = ? AND created_by = ?' ).get(docId, req.userId); if (existing) { return res.json({ share_token: existing.share_token }); } // Generate a new share token (URL-safe UUID) const shareToken = crypto.randomUUID(); db.prepare( 'INSERT INTO shared_documents (share_token, document_id, created_by) VALUES (?, ?, ?)' ).run(shareToken, docId, req.userId); console.log(`šŸ”— Share link created for doc ${docId} → token ${shareToken.substring(0, 8)}…`); res.status(201).json({ share_token: shareToken }); } catch (err) { console.error('Failed to create share link:', err); res.status(500).json({ error: err.message }); } }); // Revoke a share link app.delete('/api/documents/:id/share', requireUser, (req, res) => { try { const docId = req.params.id; // Verify ownership const doc = db.prepare( 'SELECT id FROM documents WHERE id = ? AND user_id = ?' ).get(docId, req.userId); if (!doc) { return res.status(404).json({ error: 'Document not found' }); } const info = db.prepare( 'DELETE FROM shared_documents WHERE document_id = ? AND created_by = ?' ).run(docId, req.userId); if (info.changes === 0) { return res.status(404).json({ error: 'No share link found for this document' }); } console.log(`šŸ”—āŒ Share link revoked for doc ${docId}`); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── Public shared document access (NO auth) ── // Get a shared document by token app.get('/api/shared/:token', (req, res) => { try { const token = req.params.token; // Validate token format (UUID) if (!token || typeof token !== 'string' || token.length < 10) { return res.status(400).json({ error: 'Invalid share token' }); } const share = db.prepare( 'SELECT document_id FROM shared_documents WHERE share_token = ?' ).get(token); if (!share) { return res.status(404).json({ error: 'Shared document not found or link has been revoked' }); } const doc = db.prepare( 'SELECT id, title, content, language, created_at, updated_at FROM documents WHERE id = ?' ).get(share.document_id); if (!doc) { // Document was deleted — clean up orphaned share record db.prepare('DELETE FROM shared_documents WHERE share_token = ?').run(token); return res.status(404).json({ error: 'The shared document no longer exists' }); } // Return document WITHOUT user_id (privacy) res.json({ id: doc.id, title: doc.title, content: doc.content, language: doc.language, content_length: (doc.content || '').length, created_at: doc.created_at, updated_at: doc.updated_at, }); } catch (err) { console.error('Failed to fetch shared document:', err); res.status(500).json({ error: err.message }); } }); // Save edits to a shared document by token (public — no auth) app.put('/api/shared/:token', (req, res) => { try { const token = req.params.token; const { title, content } = req.body; // Validate token format if (!token || typeof token !== 'string' || token.length < 10) { return res.status(400).json({ error: 'Invalid share token' }); } const share = db.prepare( 'SELECT document_id FROM shared_documents WHERE share_token = ?' ).get(token); if (!share) { return res.status(404).json({ error: 'Shared document not found or link has been revoked' }); } const updates = []; const params = []; if (title !== undefined) { updates.push('title = ?'); params.push(typeof title === 'string' ? title.trim() : 'Untitled Document'); } if (content !== undefined) { updates.push('content = ?'); params.push(typeof content === 'string' ? content : ''); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update' }); } updates.push('updated_at = CURRENT_TIMESTAMP'); params.push(share.document_id); db.prepare( `UPDATE documents SET ${updates.join(', ')} WHERE id = ?` ).run(...params); const updatedDoc = db.prepare( 'SELECT id, title, content, language, created_at, updated_at FROM documents WHERE id = ?' ).get(share.document_id); res.json({ id: updatedDoc.id, title: updatedDoc.title, content: updatedDoc.content, language: updatedDoc.language, content_length: (updatedDoc.content || '').length, created_at: updatedDoc.created_at, updated_at: updatedDoc.updated_at, }); } catch (err) { console.error('Failed to save shared document:', err); res.status(500).json({ error: err.message }); } }); // Beacon-save for shared documents (for page close via navigator.sendBeacon) app.post('/api/shared/:token/beacon-save', (req, res) => { try { const token = req.params.token; const { title, content } = req.body; if (!token || typeof token !== 'string' || token.length < 10) { return res.status(400).json({ error: 'Invalid share token' }); } const share = db.prepare( 'SELECT document_id FROM shared_documents WHERE share_token = ?' ).get(token); if (!share) { return res.status(404).json({ error: 'Shared document not found' }); } const updates = []; const params = []; if (title !== undefined) { updates.push('title = ?'); params.push(typeof title === 'string' ? title.trim() : 'Untitled Document'); } if (content !== undefined) { updates.push('content = ?'); params.push(typeof content === 'string' ? content : ''); } if (updates.length > 0) { updates.push('updated_at = CURRENT_TIMESTAMP'); params.push(share.document_id); db.prepare( `UPDATE documents SET ${updates.join(', ')} WHERE id = ?` ).run(...params); } res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Beacon-save endpoint (POST because sendBeacon only supports POST) // Used for saving on page close via navigator.sendBeacon() app.post('/api/documents/:id/beacon-save', requireUser, (req, res) => { try { const { title, content } = req.body; const existing = db.prepare(` SELECT id FROM documents WHERE id = ? AND user_id = ? `).get(req.params.id, req.userId); if (!existing) { return res.status(404).json({ error: 'Document not found' }); } const updates = []; const params = []; if (title !== undefined) { updates.push('title = ?'); params.push(typeof title === 'string' ? title.trim() : 'Untitled Document'); } if (content !== undefined) { updates.push('content = ?'); params.push(typeof content === 'string' ? content : ''); } if (updates.length > 0) { updates.push('updated_at = CURRENT_TIMESTAMP'); params.push(req.params.id, req.userId); db.prepare(` UPDATE documents SET ${updates.join(', ')} WHERE id = ? AND user_id = ? `).run(...params); } res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ============================================ // ERROR HANDLING MIDDLEWARE // ============================================ app.use((req, res, next) => { console.log(`āŒ Route not found: ${req.method} ${req.url}`); res.status(404).json({ error: 'Route not found', method: req.method, path: req.url }); }); app.use((err, req, res, next) => { console.error(`šŸ”„ Unhandled error:`, err); console.error(` Request: ${req.method} ${req.url}`); console.error(` User ID: ${req.userId || 'unknown'}`); res.status(500).json({ error: 'Internal server error', message: err.message, timestamp: new Date().toISOString() }); }); // ============================================ // START SERVER // ============================================ const port = process.env.PORT || 7860; app.listen(port, () => { console.log(`\nšŸš€ ===========================================`); console.log(`šŸš€ NeuralStream Chat Storage v2.0.0`); console.log(`šŸš€ Listening on port ${port}`); console.log(`šŸš€ Database: ${dbPath}`); console.log(`šŸš€ Logging: COMPREHENSIVE`); console.log(`šŸš€ ===========================================\n`); // Log startup timestamp and environment console.log(`šŸ“… Server started at: ${new Date().toISOString()}`); console.log(`šŸŒ Environment: ${process.env.NODE_ENV || 'development'}`); console.log(`šŸ’» Platform: ${process.platform}`); console.log(`šŸ‘¤ User: ${require('os').userInfo().username}`); // Test database with sample query try { const testResult = db.prepare('SELECT sqlite_version() as version').get(); console.log(`šŸ’¾ SQLite version: ${testResult.version}`); } catch (error) { console.error(`āŒ Database test failed:`, error); } }); } catch (error) { console.error(`āŒ Failed to initialize database:`, error); process.exit(1); }