Files
signal_encryption_poc/server.js
Atridad Lahiji 81d8091354
Some checks are pending
Deploy Encrypted Todo App / build-and-push (push) Has started running
Permissions issues on my deployment server... so I guess in-memory it
is!
2025-06-16 10:55:14 -06:00

247 lines
6.4 KiB
JavaScript

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}`);
});