Files
signal_encryption_poc/todo-service.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

269 lines
7.8 KiB
JavaScript

import "dotenv/config";
import { SignalClient } from "./signal-crypto.js";
import { v4 as uuidv4 } from "uuid";
export class EncryptedTodoService {
constructor() {
// In-memory storage using Maps
this.users = new Map(); // userId -> SignalClient
this.userIds = new Set(); // Set of all user IDs
this.todos = new Map(); // todoId -> todo object
this.todoGroups = new Map(); // todoGroupId -> group info
this.todoParticipants = new Map(); // todoGroupId -> Set of userIds
this.userTodos = new Map(); // userId -> Set of todoGroupIds
console.log("Initialized in-memory encrypted todo service");
}
async createUser(userId) {
this.userIds.add(userId);
if (!this.users.has(userId)) {
this.users.set(userId, SignalClient.create(userId));
this.userTodos.set(userId, new Set());
}
return { userId };
}
async getAllUsers() {
return Array.from(this.userIds);
}
async addTodo(userId, todoText) {
const todoGroupId = uuidv4();
const client = this.users.get(userId);
const todoId = uuidv4();
const encryptedTodo = client.encrypt(todoText);
const now = new Date().toISOString();
// Create the todo group
this.todoGroups.set(todoGroupId, {
todoGroupId,
createdBy: userId,
createdAt: now,
});
// Add the creator as a participant
this.todoParticipants.set(todoGroupId, new Set([userId]));
// Create the todo entry for the creator
const todoKey = `${todoGroupId}_${userId}`;
this.todos.set(todoKey, {
id: todoId,
todoGroupId,
userId,
encrypted: encryptedTodo,
createdAt: now,
originalText: todoText,
completed: false,
});
// Add to user's todo list
this.userTodos.get(userId).add(todoGroupId);
return todoGroupId;
}
async getTodos(userId) {
const client = this.users.get(userId);
if (!client) {
console.error(`No client found for user ${userId}`);
return [];
}
const userTodoGroups = this.userTodos.get(userId) || new Set();
const todos = [];
for (const todoGroupId of userTodoGroups) {
try {
const todoKey = `${todoGroupId}_${userId}`;
const todoData = this.todos.get(todoKey);
if (!todoData) {
console.warn(`Todo data not found for ${todoKey}`);
continue;
}
const decryptedText = client.decrypt(todoData.encrypted);
const groupInfo = this.todoGroups.get(todoGroupId);
const participants = Array.from(
this.todoParticipants.get(todoGroupId) || new Set(),
);
todos.push({
id: todoGroupId, // Use todoGroupId as the primary identifier
text: decryptedText,
createdAt: todoData.createdAt,
completed: todoData.completed,
participants,
createdBy: groupInfo?.createdBy || userId,
isCreator: (groupInfo?.createdBy || userId) === userId,
});
} catch (e) {
console.error(`Error processing todo ${todoGroupId}:`, e);
}
}
return todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
async getTodo(userId, todoGroupId) {
const client = this.users.get(userId);
const todoKey = `${todoGroupId}_${userId}`;
const todoData = this.todos.get(todoKey);
if (!todoData) throw new Error("Todo not found");
const decryptedText = client.decrypt(todoData.encrypted);
const groupInfo = this.todoGroups.get(todoGroupId);
const participants = Array.from(
this.todoParticipants.get(todoGroupId) || new Set(),
);
return {
id: todoGroupId,
text: decryptedText,
createdAt: todoData.createdAt,
completed: todoData.completed,
participants,
createdBy: groupInfo?.createdBy,
isCreator: groupInfo?.createdBy === userId,
};
}
async updateTodoStatus(userId, todoGroupId, completed) {
console.log(
`updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`,
);
const participants = this.todoParticipants.get(todoGroupId);
if (!participants || !participants.has(userId)) {
throw new Error("User is not a participant in this todo group");
}
const participantIds = Array.from(participants);
console.log(`Updating todo for all participants:`, participantIds);
// Update todos for all participants
for (const participantId of participantIds) {
const todoKey = `${todoGroupId}_${participantId}`;
const todoData = this.todos.get(todoKey);
if (todoData) {
todoData.completed = completed;
this.todos.set(todoKey, todoData);
}
}
console.log(`Affected users for todo update:`, participantIds);
return participantIds;
}
async deleteTodo(userId, todoGroupId) {
const participants = this.todoParticipants.get(todoGroupId);
if (!participants || !participants.has(userId)) {
throw new Error("User is not a participant in this todo group");
}
const groupInfo = this.todoGroups.get(todoGroupId);
if (!groupInfo) {
throw new Error("Todo group not found");
}
if (groupInfo.createdBy !== userId) {
throw new Error("Only the creator can delete this todo");
}
const participantIds = Array.from(participants);
console.log(
`Deleting todo group ${todoGroupId} for all participants:`,
participantIds,
);
// Delete all todo entries for this group
for (const participantId of participantIds) {
const todoKey = `${todoGroupId}_${participantId}`;
this.todos.delete(todoKey);
// Remove from user's todo list
const userTodoSet = this.userTodos.get(participantId);
if (userTodoSet) {
userTodoSet.delete(todoGroupId);
}
}
// Delete participants and group
this.todoParticipants.delete(todoGroupId);
this.todoGroups.delete(todoGroupId);
return {
deletedIds: [todoGroupId],
affectedUsers: participantIds,
};
}
async shareTodo(userId, todoGroupId, recipientId) {
// Ensure recipient user exists
if (!this.users.has(recipientId)) {
this.users.set(recipientId, SignalClient.create(recipientId));
this.userTodos.set(recipientId, new Set());
this.userIds.add(recipientId);
}
const participants = this.todoParticipants.get(todoGroupId);
if (!participants || !participants.has(userId)) {
throw new Error("User is not a participant in this todo group");
}
const groupInfo = this.todoGroups.get(todoGroupId);
if (!groupInfo) {
throw new Error("Todo group not found");
}
if (groupInfo.createdBy !== userId) {
throw new Error("Only the creator can add participants to this todo");
}
if (participants.has(recipientId)) {
throw new Error("User is already a participant in this todo group");
}
// Get the original text from the sender's todo
const senderTodoKey = `${todoGroupId}_${userId}`;
const senderTodoData = this.todos.get(senderTodoKey);
if (!senderTodoData) throw new Error("Todo not found");
const recipientClient = this.users.get(recipientId);
const encryptedForRecipient = recipientClient.encrypt(
senderTodoData.originalText,
);
const recipientTodoId = uuidv4();
const now = new Date().toISOString();
// Add recipient as a participant
participants.add(recipientId);
this.todoParticipants.set(todoGroupId, participants);
// Create todo entry for the recipient
const recipientTodoKey = `${todoGroupId}_${recipientId}`;
this.todos.set(recipientTodoKey, {
id: recipientTodoId,
todoGroupId,
userId: recipientId,
encrypted: encryptedForRecipient,
createdAt: now,
originalText: senderTodoData.originalText,
completed: senderTodoData.completed,
});
// Add to recipient's todo list
this.userTodos.get(recipientId).add(todoGroupId);
return todoGroupId;
}
}