Some checks are pending
Deploy Encrypted Todo App / build-and-push (push) Has started running
is!
247 lines
6.4 KiB
JavaScript
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}`);
|
|
});
|