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`); }); ws.on("error", (error) => { console.error(`WebSocket error for user ${userId}:`, error); connections.delete(userId); }); } }); // Helper function to broadcast to all connected users function broadcast(message) { const messageStr = JSON.stringify(message); connections.forEach((ws, userId) => { if (ws.readyState === WebSocket.OPEN) { ws.send(messageStr); } }); } // Helper function to broadcast to a specific user function broadcastToUser(userId, message) { const ws = connections.get(userId); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } } app.use(express.json()); app.use(express.static("public")); // Middleware to handle async errors const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; // Error handling middleware app.use((err, req, res, next) => { console.error("Error:", err); res.status(500).json({ error: err.message || "Internal server error" }); }); // Create a new user app.post( "/api/users", asyncHandler(async (req, res) => { const { userId } = req.body; const user = await todoService.createUser(userId); const users = await todoService.getAllUsers(); broadcast({ type: "users", data: users }); res.json(user); }), ); // Get all users app.get( "/api/users", asyncHandler(async (req, res) => { const users = await todoService.getAllUsers(); res.json(users); }), ); // Add a new todo app.post( "/api/users/:userId/todos", asyncHandler(async (req, res) => { const { userId } = req.params; const { text } = req.body; const todoGroupId = await todoService.addTodo(userId, text); const todo = await todoService.getTodo(userId, todoGroupId); broadcastToUser(userId, { type: "todo_added", data: todo }); res.json({ id: todoGroupId }); }), ); // Get todos for a user app.get( "/api/users/:userId/todos", asyncHandler(async (req, res) => { const { userId } = req.params; const todos = await todoService.getTodos(userId); res.json(todos); }), ); // Get encrypted todos (for debugging) app.get( "/api/users/:userId/todos/encrypted", asyncHandler(async (req, res) => { const { userId } = req.params; const todos = await todoService.getTodos(userId); const encryptedTodos = []; for (const todo of todos) { const todoKey = `${todo.id}_${userId}`; const todoData = todoService.todos.get(todoKey); if (!todoData) { console.warn( `No encrypted data found for todo ${todo.id} and user ${userId}`, ); continue; } encryptedTodos.push({ id: todo.id, encrypted: todoData.encrypted, createdAt: todoData.createdAt, participants: todo.participants, }); } res.json(encryptedTodos); }), ); // Delete a todo app.delete( "/api/users/:userId/todos/:todoGroupId", asyncHandler(async (req, res) => { const { userId, todoGroupId } = req.params; const { deletedIds, affectedUsers } = await todoService.deleteTodo( userId, todoGroupId, ); // Broadcast deletion to all affected users console.log(`Broadcasting deletion to users: ${affectedUsers.join(", ")}`); affectedUsers.forEach((affectedUserId) => { deletedIds.forEach((deletedId) => { broadcastToUser(affectedUserId, { type: "todo_deleted", data: { id: deletedId, deletedBy: userId }, }); }); }); res.json({ deletedIds, affectedUsers }); }), ); // Share a todo with another user app.post( "/api/users/:userId/todos/:todoGroupId/share", asyncHandler(async (req, res) => { const { userId, todoGroupId } = req.params; const { recipientId } = req.body; if (!recipientId) { return res.status(400).json({ error: "Recipient ID is required" }); } const sharedTodoGroupId = await todoService.shareTodo( userId, todoGroupId, recipientId, ); const sharedTodo = await todoService.getTodo( recipientId, sharedTodoGroupId, ); broadcastToUser(recipientId, { type: "todo_shared", data: sharedTodo }); res.json({ sharedTodoGroupId, message: `Todo shared with ${recipientId}`, }); }), ); // Update todo completion status app.patch( "/api/users/:userId/todos/:todoGroupId", asyncHandler(async (req, res) => { const { userId, todoGroupId } = req.params; const { completed } = req.body; console.log( `PATCH request: user ${userId} updating todo ${todoGroupId} to completed=${completed}`, ); const affectedUsers = await todoService.updateTodoStatus( userId, todoGroupId, completed, ); console.log(`Broadcasting update to users: ${affectedUsers.join(", ")}`); // Broadcast the update to all affected users for (const affectedUserId of affectedUsers) { try { const todo = await todoService.getTodo(affectedUserId, todoGroupId); const message = { type: "todo_updated", data: { id: todoGroupId, completed, updatedBy: userId }, }; broadcastToUser(affectedUserId, message); console.log(`Sent update to user ${affectedUserId}:`, message); } catch (error) { console.error( `Error getting todo for user ${affectedUserId}:`, error.message, ); } } res.json({ message: "Todo updated successfully", affectedUsers }); }), ); const PORT = process.env.APP_PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });