naf / server.js
Rajhuggingface4253's picture
Update server.js
08ad63a verified
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);
}