:)
Some checks failed
Deploy Encrypted Todo App / build-and-push (push) Has been cancelled

This commit is contained in:
2025-06-16 09:16:22 -06:00
parent 2f08b94c89
commit d71062cf13
13 changed files with 1817 additions and 1160 deletions

223
server.js
View File

@ -1,31 +1,88 @@
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", async (req, res) => {
app.post("/api/users", (req, res) => {
try {
const { userId } = req.body;
if (!userId) {
return res.status(400).json({ error: "userId is required" });
}
const user = await todoService.createUser(userId);
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 (for sharing)
app.get("/api/users", async (req, res) => {
// Get all users
app.get("/api/users", (req, res) => {
try {
const users = Array.from(todoService.users.keys());
const users = todoService.getAllUsers();
res.json(users);
} catch (error) {
res.status(400).json({ error: error.message });
@ -33,27 +90,24 @@ app.get("/api/users", async (req, res) => {
});
// Add todo
app.post("/api/users/:userId/todos", async (req, res) => {
app.post("/api/users/:userId/todos", (req, res) => {
try {
const { userId } = req.params;
const { text } = req.body;
if (!text) {
return res.status(400).json({ error: "Todo text is required" });
}
const todoId = await todoService.addTodo(userId, text);
res.json({ todoId });
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 (decrypted)
app.get("/api/users/:userId/todos", async (req, res) => {
// Get todos
app.get("/api/users/:userId/todos", (req, res) => {
try {
const { userId } = req.params;
const todos = await todoService.getTodos(userId);
const todos = todoService.getTodos(userId);
res.json(todos);
} catch (error) {
res.status(400).json({ error: error.message });
@ -61,62 +115,143 @@ app.get("/api/users/:userId/todos", async (req, res) => {
});
// Get encrypted todos
app.get("/api/users/:userId/todos/encrypted", async (req, res) => {
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);
// Get the user's todo IDs
const todoIds = todoService.userTodos.get(userId) || new Set();
// Get the encrypted todos
const encryptedTodos = Array.from(todoIds)
.map((todoId) => {
const todo = todoService.todos.get(todoId);
if (!todo) return null;
if (!row) {
console.warn(
`No encrypted data found for todo ${todo.id} and user ${userId}`,
);
return {
id: todoId,
encrypted: todo.encrypted,
id: todo.id,
encrypted: { body: "Encryption data not available" },
createdAt: todo.createdAt,
};
})
.filter((todo) => todo !== null);
}
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/:todoId", async (req, res) => {
app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => {
try {
const { userId, todoId } = req.params;
await todoService.deleteTodo(userId, todoId);
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 with another user
app.post("/api/users/:userId/todos/:todoId/share", async (req, res) => {
// Share todo
app.post("/api/users/:userId/todos/:todoGroupId/share", (req, res) => {
try {
const { userId, todoId } = req.params;
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 },
});
await todoService.shareTodo(userId, todoId, 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;
app.listen(PORT, () => {
console.log(`Encrypted Todo server running on port ${PORT}`);
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});