import express from "express"; import { EncryptedTodoService } from "./todo-service.js"; import { WebSocketServer, WebSocket } from "ws"; import http from "http"; import "dotenv/config"; const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server }); const todoService = new EncryptedTodoService(); // Store active WebSocket connections const connections = new Map(); // WebSocket connection handler wss.on("connection", (ws, req) => { const userId = req.url.split("?userId=")[1]; if (userId) { connections.set(userId, ws); console.log(`User ${userId} connected`); // Send initial user list ws.send( JSON.stringify({ type: "users", data: Array.from(todoService.users.keys()), }), ); ws.on("close", () => { connections.delete(userId); console.log(`User ${userId} disconnected`); }); } }); // Broadcast to all connected clients function broadcast(message) { const messageStr = JSON.stringify(message); connections.forEach((ws) => { if (ws.readyState === WebSocket.OPEN) { ws.send(messageStr); } }); } // Broadcast to specific user function broadcastToUser(userId, message) { const ws = connections.get(userId); console.log( `broadcastToUser called for ${userId}, connection exists: ${!!ws}, readyState: ${ws ? ws.readyState : "N/A"}`, ); if (ws && ws.readyState === WebSocket.OPEN) { console.log( `✓ Sending WebSocket message to ${userId}:`, message.type, JSON.stringify(message.data), ); ws.send(JSON.stringify(message)); } else { console.log( `✗ WebSocket not available for user ${userId} - connection: ${!!ws}, readyState: ${ws ? ws.readyState : "N/A"}`, ); } } app.use(express.json()); app.use(express.static("public")); // Create user app.post("/api/users", (req, res) => { try { const { userId } = req.body; const user = todoService.createUser(userId); broadcast({ type: "users", data: todoService.getAllUsers() }); res.json(user); } catch (error) { res.status(400).json({ error: error.message }); } }); // Get all users app.get("/api/users", (req, res) => { try { const users = todoService.getAllUsers(); res.json(users); } catch (error) { res.status(400).json({ error: error.message }); } }); // Add todo app.post("/api/users/:userId/todos", (req, res) => { try { const { userId } = req.params; const { text } = req.body; const todoGroupId = todoService.addTodo(userId, text); const todo = todoService.getTodo(userId, todoGroupId); broadcastToUser(userId, { type: "todo_added", data: todo }); res.json({ id: todoGroupId }); } catch (error) { res.status(400).json({ error: error.message }); } }); // Get todos app.get("/api/users/:userId/todos", (req, res) => { try { const { userId } = req.params; const todos = todoService.getTodos(userId); res.json(todos); } catch (error) { res.status(400).json({ error: error.message }); } }); // Get encrypted todos app.get("/api/users/:userId/todos/encrypted", (req, res) => { try { const { userId } = req.params; const todos = todoService.getTodos(userId); const encryptedTodos = todos.map((todo) => { const row = todoService.db .prepare( "SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?", ) .get(todo.id, userId); if (!row) { console.warn( `No encrypted data found for todo ${todo.id} and user ${userId}`, ); return { id: todo.id, encrypted: { body: "Encryption data not available" }, createdAt: todo.createdAt, }; } return { id: todo.id, encrypted: JSON.parse(row.encrypted), createdAt: row.createdAt, }; }); res.json(encryptedTodos); } catch (error) { console.error("Error in encrypted todos endpoint:", error); res.status(400).json({ error: error.message }); } }); // Delete todo app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => { try { const { userId, todoGroupId } = req.params; const { deletedIds, affectedUsers } = todoService.deleteTodo( userId, todoGroupId, ); // Broadcast deletion to all affected users console.log(`Broadcasting deletion to users: ${affectedUsers.join(", ")}`); affectedUsers.forEach((affectedUserId) => { deletedIds.forEach((deletedId) => { console.log( `Sending todo_deleted event for todo ${deletedId} to user ${affectedUserId}`, ); broadcastToUser(affectedUserId, { type: "todo_deleted", data: { id: deletedId, deletedBy: userId }, }); }); }); res.json({ success: true }); } catch (error) { res.status(400).json({ error: error.message }); } }); // Share todo app.post("/api/users/:userId/todos/:todoGroupId/share", (req, res) => { try { const { userId, todoGroupId } = req.params; const { recipientId } = req.body; if (!recipientId) { return res.status(400).json({ error: "Recipient ID is required" }); } const sharedTodoGroupId = todoService.shareTodo( userId, todoGroupId, recipientId, ); const sharedTodo = todoService.getTodo(recipientId, sharedTodoGroupId); broadcastToUser(recipientId, { type: "todo_shared", data: sharedTodo }); // Also notify the original user that the todo was shared broadcastToUser(userId, { type: "todo_share_confirmed", data: { todoGroupId, sharedWith: recipientId }, }); res.json({ success: true }); } catch (error) { res.status(400).json({ error: error.message }); } }); // Update todo completion status app.patch("/api/users/:userId/todos/:todoGroupId", (req, res) => { try { const { userId, todoGroupId } = req.params; const { completed } = req.body; console.log( `PATCH request: user ${userId} updating todo ${todoGroupId} to completed=${completed}`, ); const affectedUsers = todoService.updateTodoStatus( userId, todoGroupId, completed, ); console.log(`updateTodoStatus returned affected users:`, affectedUsers); // Broadcast update to all affected users console.log( `Broadcasting todo update to users: ${affectedUsers.join(", ")}`, ); affectedUsers.forEach((affectedUserId) => { console.log( `Sending todo_updated event for todo ${todoGroupId} to user ${affectedUserId}`, ); const message = { type: "todo_updated", data: { id: todoGroupId, completed, updatedBy: userId }, }; console.log( `Message being sent to ${affectedUserId}:`, JSON.stringify(message), ); broadcastToUser(affectedUserId, message); }); res.json({ success: true }); } catch (error) { console.error(`Error updating todo status:`, error); res.status(400).json({ error: error.message }); } }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });