From 81d80913549733242117ca05f9fa0c562a30c988 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Mon, 16 Jun 2025 10:55:14 -0600 Subject: [PATCH] Permissions issues on my deployment server... so I guess in-memory it is! --- Dockerfile | 8 - docker-compose.yml | 24 +-- package-lock.json | 345 ------------------------------------ package.json | 1 - server.js | 12 +- todo-service.js | 432 ++++++++++++--------------------------------- validate-build.sh | 130 -------------- 7 files changed, 122 insertions(+), 830 deletions(-) delete mode 100755 validate-build.sh diff --git a/Dockerfile b/Dockerfile index ea8114e..fe8f73f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,21 +9,13 @@ RUN apt-get update && apt-get install -y \ libc6 \ && rm -rf /var/lib/apt/lists/* -# Copy dependency files first for better caching COPY package.json package-lock.json ./ -# Install dependencies RUN npm install --prod -# Copy application files COPY server.js todo-service.js signal-crypto.js ./ COPY public/ ./public/ -# Expose port EXPOSE 3000 -# Switch to non-root user -USER node - -# Start application CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 28eb6a4..28af8bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,31 +2,11 @@ version: "3.8" services: app: - build: - context: . - dockerfile: Dockerfile + image: ${IMAGE} ports: - "${APP_PORT:-3000}:3000" environment: NODE_ENV: production - SQLITE_DB_PATH: /app/data/db.db - APP_PORT: 3000 volumes: - - ./data:/app/data + - ${ROOT_DIR}:/app/data restart: unless-stopped - healthcheck: - test: - [ - "CMD", - "node", - "-e", - "const http = require('http'); const req = http.request({hostname: 'localhost', port: 3000, path: '/api/users', method: 'GET'}, (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => process.exit(1)); req.end();", - ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - -volumes: - data: - driver: local diff --git a/package-lock.json b/package-lock.json index 5cb2d39..6febc7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "signal_encrypted_poc", "version": "1.0.0", "dependencies": { - "@libsql/client": "^0.15.9", "@signalapp/libsignal-client": "^0.74.1", "dotenv": "^16.5.0", "express": "^5.1.0", @@ -16,182 +15,6 @@ "ws": "^8.18.2" } }, - "node_modules/@libsql/client": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.15.9.tgz", - "integrity": "sha512-VT3do0a0vwYVaNcp/y05ikkKS3OrFR5UeEf5SUuYZVgKVl1Nc1k9ajoYSsOid8AD/vlhLDB5yFQaV4HmT/OB9w==", - "license": "MIT", - "dependencies": { - "@libsql/core": "^0.15.9", - "@libsql/hrana-client": "^0.7.0", - "js-base64": "^3.7.5", - "libsql": "^0.5.13", - "promise-limit": "^2.7.0" - } - }, - "node_modules/@libsql/core": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.15.9.tgz", - "integrity": "sha512-4OVdeAmuaCUq5hYT8NNn0nxlO9AcA/eTjXfUZ+QK8MT3Dz7Z76m73x7KxjU6I64WyXX98dauVH2b9XM+d84npw==", - "license": "MIT", - "dependencies": { - "js-base64": "^3.7.5" - } - }, - "node_modules/@libsql/darwin-arm64": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.13.tgz", - "integrity": "sha512-ASz/EAMLDLx3oq9PVvZ4zBXXHbz2TxtxUwX2xpTRFR4V4uSHAN07+jpLu3aK5HUBLuv58z7+GjaL5w/cyjR28Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@libsql/darwin-x64": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.13.tgz", - "integrity": "sha512-kzglniv1difkq8opusSXM7u9H0WoEPeKxw0ixIfcGfvlCVMJ+t9UNtXmyNHW68ljdllje6a4C6c94iPmIYafYA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@libsql/hrana-client": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz", - "integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==", - "license": "MIT", - "dependencies": { - "@libsql/isomorphic-fetch": "^0.3.1", - "@libsql/isomorphic-ws": "^0.1.5", - "js-base64": "^3.7.5", - "node-fetch": "^3.3.2" - } - }, - "node_modules/@libsql/isomorphic-fetch": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz", - "integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@libsql/isomorphic-ws": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", - "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", - "license": "MIT", - "dependencies": { - "@types/ws": "^8.5.4", - "ws": "^8.13.0" - } - }, - "node_modules/@libsql/linux-arm-gnueabihf": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.13.tgz", - "integrity": "sha512-UEW+VZN2r0mFkfztKOS7cqfS8IemuekbjUXbXCwULHtusww2QNCXvM5KU9eJCNE419SZCb0qaEWYytcfka8qeA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-arm-musleabihf": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.13.tgz", - "integrity": "sha512-NMDgLqryYBv4Sr3WoO/m++XDjR5KLlw9r/JK4Ym6A1XBv2bxQQNhH0Lxx3bjLW8qqhBD4+0xfms4d2cOlexPyA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-arm64-gnu": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.13.tgz", - "integrity": "sha512-/wCxVdrwl1ee6D6LEjwl+w4SxuLm5UL9Kb1LD5n0bBGs0q+49ChdPPh7tp175iRgkcrTgl23emymvt1yj3KxVQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-arm64-musl": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.13.tgz", - "integrity": "sha512-xnVAbZIanUgX57XqeI5sNaDnVilp0Di5syCLSEo+bRyBobe/1IAeehNZpyVbCy91U2N6rH1C/mZU7jicVI9x+A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-x64-gnu": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.13.tgz", - "integrity": "sha512-/mfMRxcQAI9f8t7tU3QZyh25lXgXKzgin9B9TOSnchD73PWtsVhlyfA6qOCfjQl5kr4sHscdXD5Yb3KIoUgrpQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-x64-musl": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.13.tgz", - "integrity": "sha512-rdefPTpQCVwUjIQYbDLMv3qpd5MdrT0IeD0UZPGqhT9AWU8nJSQoj2lfyIDAWEz7PPOVCY4jHuEn7FS2sw9kRA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/win32-x64-msvc": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.13.tgz", - "integrity": "sha512-aNcmDrD1Ws+dNZIv9ECbxBQumqB9MlSVEykwfXJpqv/593nABb8Ttg5nAGUPtnADyaGDTrGvPPP81d/KsKho4Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@neon-rs/load": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", - "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", - "license": "MIT" - }, "node_modules/@signalapp/libsignal-client": { "version": "0.74.1", "resolved": "https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.74.1.tgz", @@ -204,24 +27,6 @@ "uuid": "^11" } }, - "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -332,15 +137,6 @@ "node": ">=6.6.0" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -367,15 +163,6 @@ "node": ">= 0.8" } }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -504,29 +291,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -544,18 +308,6 @@ "node": ">= 0.8" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -714,44 +466,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", - "license": "BSD-3-Clause" - }, - "node_modules/libsql": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.13.tgz", - "integrity": "sha512-5Bwoa/CqzgkTwySgqHA5TsaUDRrdLIbdM4egdPcaAnqO3aC+qAgS6BwdzuZwARA5digXwiskogZ8H7Yy4XfdOg==", - "cpu": [ - "x64", - "arm64", - "wasm32", - "arm" - ], - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32" - ], - "dependencies": { - "@neon-rs/load": "^0.0.4", - "detect-libc": "2.0.2" - }, - "optionalDependencies": { - "@libsql/darwin-arm64": "0.5.13", - "@libsql/darwin-x64": "0.5.13", - "@libsql/linux-arm-gnueabihf": "0.5.13", - "@libsql/linux-arm-musleabihf": "0.5.13", - "@libsql/linux-arm64-gnu": "0.5.13", - "@libsql/linux-arm64-musl": "0.5.13", - "@libsql/linux-x64-gnu": "0.5.13", - "@libsql/linux-x64-musl": "0.5.13", - "@libsql/win32-x64-msvc": "0.5.13" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -818,44 +532,6 @@ "node": ">= 0.6" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -918,12 +594,6 @@ "node": ">=16" } }, - "node_modules/promise-limit": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", - "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", - "license": "ISC" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1177,12 +847,6 @@ "node": ">= 0.6" } }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1214,15 +878,6 @@ "node": ">= 0.8" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index b52253b..a80dce3 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "type": "module", "dependencies": { - "@libsql/client": "^0.15.9", "@signalapp/libsignal-client": "^0.74.1", "dotenv": "^16.5.0", "express": "^5.1.0", diff --git a/server.js b/server.js index 46c78a5..a2d1f85 100644 --- a/server.js +++ b/server.js @@ -124,12 +124,10 @@ app.get( const encryptedTodos = []; for (const todo of todos) { - const row = await todoService.db.execute( - "SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?", - [todo.id, userId], - ); + const todoKey = `${todo.id}_${userId}`; + const todoData = todoService.todos.get(todoKey); - if (row.rows.length === 0) { + if (!todoData) { console.warn( `No encrypted data found for todo ${todo.id} and user ${userId}`, ); @@ -138,8 +136,8 @@ app.get( encryptedTodos.push({ id: todo.id, - encrypted: row.rows[0].encrypted, - createdAt: row.rows[0].createdAt, + encrypted: todoData.encrypted, + createdAt: todoData.createdAt, participants: todo.participants, }); } diff --git a/todo-service.js b/todo-service.js index fe33872..72a4fd9 100644 --- a/todo-service.js +++ b/todo-service.js @@ -1,187 +1,31 @@ -import { createClient } from "@libsql/client"; import "dotenv/config"; import { SignalClient } from "./signal-crypto.js"; import { v4 as uuidv4 } from "uuid"; export class EncryptedTodoService { constructor() { - this.db = createClient({ - url: `file:${process.env.SQLITE_DB_PATH}`, - }); - this.users = new Map(); + // 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 - // Initialize database and migrate - this.initializeDatabase(); - } - - async initializeDatabase() { - // Create tables with new schema - await this.db.execute(` - CREATE TABLE IF NOT EXISTS users ( - userId TEXT PRIMARY KEY - ); - `); - - await this.db.execute(` - CREATE TABLE IF NOT EXISTS todos ( - id TEXT PRIMARY KEY, - todoGroupId TEXT, - userId TEXT, - encrypted TEXT, - createdAt TEXT, - originalText TEXT, - completed INTEGER, - sharedBy TEXT, - originalTodoId TEXT, - FOREIGN KEY(userId) REFERENCES users(userId) - ); - `); - - await this.db.execute(` - CREATE TABLE IF NOT EXISTS todo_groups ( - todoGroupId TEXT PRIMARY KEY, - createdBy TEXT, - createdAt TEXT, - FOREIGN KEY(createdBy) REFERENCES users(userId) - ); - `); - - await this.db.execute(` - CREATE TABLE IF NOT EXISTS todo_participants ( - todoGroupId TEXT, - userId TEXT, - joinedAt TEXT, - PRIMARY KEY(todoGroupId, userId), - FOREIGN KEY(todoGroupId) REFERENCES todo_groups(todoGroupId), - FOREIGN KEY(userId) REFERENCES users(userId) - ); - `); - - await this.db.execute(` - CREATE TABLE IF NOT EXISTS todo_shares ( - todoId TEXT, - sharedWith TEXT, - FOREIGN KEY(todoId) REFERENCES todos(id), - FOREIGN KEY(sharedWith) REFERENCES users(userId) - ); - `); - - // Migrate existing data to new schema - await this.migrateToNewSchema(); - - // Load existing users from database on startup - await this.loadExistingUsers(); - } - - async migrateToNewSchema() { - // Check if we need to migrate by looking for todos without todoGroupId - const oldTodos = await this.db.execute( - "SELECT * FROM todos WHERE todoGroupId IS NULL", - ); - - if (oldTodos.rows.length > 0) { - console.log(`Migrating ${oldTodos.rows.length} todos to new schema...`); - - for (const todo of oldTodos.rows) { - const todoGroupId = uuidv4(); - const now = new Date().toISOString(); - - // If this is an original todo (not shared) - if (!todo.sharedBy && !todo.originalTodoId) { - // Create todo group - await this.db.execute( - "INSERT OR IGNORE INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", - [todoGroupId, todo.userId, todo.createdAt || now], - ); - - // Add creator as participant - await this.db.execute( - "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - [todoGroupId, todo.userId, todo.createdAt || now], - ); - - // Update the todo with todoGroupId - await this.db.execute( - "UPDATE todos SET todoGroupId = ? WHERE id = ?", - [todoGroupId, todo.id], - ); - - // Find and migrate shared copies - const sharedCopies = await this.db.execute( - "SELECT * FROM todos WHERE originalTodoId = ?", - [todo.id], - ); - - for (const sharedTodo of sharedCopies.rows) { - // Add shared user as participant - await this.db.execute( - "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - [todoGroupId, sharedTodo.userId, sharedTodo.createdAt || now], - ); - - // Update shared todo with same todoGroupId - await this.db.execute( - "UPDATE todos SET todoGroupId = ? WHERE id = ?", - [todoGroupId, sharedTodo.id], - ); - } - } - // If this is a shared todo without a group (shouldn't happen after above, but just in case) - else if (todo.originalTodoId && !todo.todoGroupId) { - const originalTodo = await this.db.execute( - "SELECT todoGroupId FROM todos WHERE id = ?", - [todo.originalTodoId], - ); - - if ( - originalTodo.rows.length > 0 && - originalTodo.rows[0].todoGroupId - ) { - await this.db.execute( - "UPDATE todos SET todoGroupId = ? WHERE id = ?", - [originalTodo.rows[0].todoGroupId, todo.id], - ); - - // Add as participant if not already - await this.db.execute( - "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - [ - originalTodo.rows[0].todoGroupId, - todo.userId, - todo.createdAt || now, - ], - ); - } - } - } - - console.log("Migration completed!"); - } - } - - async loadExistingUsers() { - const existingUsers = await this.db.execute("SELECT userId FROM users"); - for (const user of existingUsers.rows) { - if (!this.users.has(user.userId)) { - this.users.set(user.userId, SignalClient.create(user.userId)); - console.log(`Loaded existing user: ${user.userId}`); - } - } + console.log("Initialized in-memory encrypted todo service"); } async createUser(userId) { - await this.db.execute("INSERT OR IGNORE INTO users (userId) VALUES (?)", [ - 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() { - const result = await this.db.execute("SELECT userId FROM users"); - return result.rows.map((u) => u.userId); + return Array.from(this.userIds); } async addTodo(userId, todoText) { @@ -192,30 +36,29 @@ export class EncryptedTodoService { const now = new Date().toISOString(); // Create the todo group - await this.db.execute( - "INSERT INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", - [todoGroupId, userId, now], - ); + this.todoGroups.set(todoGroupId, { + todoGroupId, + createdBy: userId, + createdAt: now, + }); // Add the creator as a participant - await this.db.execute( - "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - [todoGroupId, userId, now], - ); + this.todoParticipants.set(todoGroupId, new Set([userId])); // Create the todo entry for the creator - await this.db.execute( - "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", - [ - todoId, - todoGroupId, - userId, - JSON.stringify(encryptedTodo), - now, - todoText, - 0, - ], - ); + 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; } @@ -227,84 +70,63 @@ export class EncryptedTodoService { return []; } - const rows = await this.db.execute("SELECT * FROM todos WHERE userId = ?", [ - userId, - ]); + const userTodoGroups = this.userTodos.get(userId) || new Set(); const todos = []; - for (const row of rows.rows) { + for (const todoGroupId of userTodoGroups) { try { - // Skip todos without todoGroupId (migration didn't work) - if (!row.todoGroupId) { - console.warn(`Todo ${row.id} has no todoGroupId, skipping`); + 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(JSON.parse(row.encrypted)); - - // Get all participants in this todo group - const participants = await this.db.execute( - "SELECT userId FROM todo_participants WHERE todoGroupId = ?", - [row.todoGroupId], - ); - - // Get group creator - const groupInfo = await this.db.execute( - "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", - [row.todoGroupId], + 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: row.todoGroupId, // Use todoGroupId as the primary identifier + id: todoGroupId, // Use todoGroupId as the primary identifier text: decryptedText, - createdAt: row.createdAt, - completed: !!row.completed, - participants: - participants.rows.length > 0 - ? participants.rows.map((p) => p.userId) - : [userId], // Fallback to current user - createdBy: groupInfo.rows[0]?.createdBy || userId, // Fallback to current user - isCreator: (groupInfo.rows[0]?.createdBy || userId) === userId, + createdAt: todoData.createdAt, + completed: todoData.completed, + participants, + createdBy: groupInfo?.createdBy || userId, + isCreator: (groupInfo?.createdBy || userId) === userId, }); } catch (e) { - console.error(`Error processing todo ${row.id}:`, 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 row = await this.db.execute( - "SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?", - [todoGroupId, userId], - ); + const todoKey = `${todoGroupId}_${userId}`; + const todoData = this.todos.get(todoKey); - if (row.rows.length === 0) throw new Error("Todo not found"); + if (!todoData) throw new Error("Todo not found"); - const todoRow = row.rows[0]; - const decryptedText = client.decrypt(JSON.parse(todoRow.encrypted)); - - // Get all participants in this todo group - const participants = await this.db.execute( - "SELECT userId FROM todo_participants WHERE todoGroupId = ?", - [todoGroupId], - ); - - // Get group creator - const groupInfo = await this.db.execute( - "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", - [todoGroupId], + 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: todoRow.createdAt, - completed: !!todoRow.completed, - participants: participants.rows.map((p) => p.userId), - createdBy: groupInfo.rows[0]?.createdBy, - isCreator: groupInfo.rows[0]?.createdBy === userId, + createdAt: todoData.createdAt, + completed: todoData.completed, + participants, + createdBy: groupInfo?.createdBy, + isCreator: groupInfo?.createdBy === userId, }; } @@ -313,26 +135,23 @@ export class EncryptedTodoService { `updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`, ); - // Get all participants in this todo group - const participants = await this.db.execute( - "SELECT userId FROM todo_participants WHERE todoGroupId = ?", - [todoGroupId], - ); + const participants = this.todoParticipants.get(todoGroupId); - const participantIds = participants.rows.map((p) => p.userId); - - if (!participantIds.includes(userId)) { + 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) { - await this.db.execute( - "UPDATE todos SET completed = ? WHERE todoGroupId = ? AND userId = ?", - [completed ? 1 : 0, todoGroupId, participantId], - ); + 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); @@ -340,52 +159,43 @@ export class EncryptedTodoService { } async deleteTodo(userId, todoGroupId) { - // Get all participants in this todo group - const participants = await this.db.execute( - "SELECT userId FROM todo_participants WHERE todoGroupId = ?", - [todoGroupId], - ); + const participants = this.todoParticipants.get(todoGroupId); - const participantIds = participants.rows.map((p) => p.userId); - - if (!participantIds.includes(userId)) { + if (!participants || !participants.has(userId)) { throw new Error("User is not a participant in this todo group"); } - // Check if user is the creator of this todo group - const groupInfo = await this.db.execute( - "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", - [todoGroupId], - ); + const groupInfo = this.todoGroups.get(todoGroupId); - if (groupInfo.rows.length === 0) { + if (!groupInfo) { throw new Error("Todo group not found"); } - if (groupInfo.rows[0].createdBy !== userId) { + 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 - await this.db.execute("DELETE FROM todos WHERE todoGroupId = ?", [ - todoGroupId, - ]); + for (const participantId of participantIds) { + const todoKey = `${todoGroupId}_${participantId}`; + this.todos.delete(todoKey); - // Delete all participants - await this.db.execute( - "DELETE FROM todo_participants WHERE todoGroupId = ?", - [todoGroupId], - ); + // Remove from user's todo list + const userTodoSet = this.userTodos.get(participantId); + if (userTodoSet) { + userTodoSet.delete(todoGroupId); + } + } - // Delete the group itself - await this.db.execute("DELETE FROM todo_groups WHERE todoGroupId = ?", [ - todoGroupId, - ]); + // Delete participants and group + this.todoParticipants.delete(todoGroupId); + this.todoGroups.delete(todoGroupId); return { deletedIds: [todoGroupId], @@ -397,73 +207,61 @@ export class EncryptedTodoService { // 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); } - // Check if user is a participant in this todo group - const participants = await this.db.execute( - "SELECT userId FROM todo_participants WHERE todoGroupId = ?", - [todoGroupId], - ); + const participants = this.todoParticipants.get(todoGroupId); - const participantIds = participants.rows.map((p) => p.userId); - - if (!participantIds.includes(userId)) { + if (!participants || !participants.has(userId)) { throw new Error("User is not a participant in this todo group"); } - // Check if user is the creator of this todo group - const groupInfo = await this.db.execute( - "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", - [todoGroupId], - ); + const groupInfo = this.todoGroups.get(todoGroupId); - if (groupInfo.rows.length === 0) { + if (!groupInfo) { throw new Error("Todo group not found"); } - if (groupInfo.rows[0].createdBy !== userId) { + if (groupInfo.createdBy !== userId) { throw new Error("Only the creator can add participants to this todo"); } - if (participantIds.includes(recipientId)) { + 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 senderTodo = await this.db.execute( - "SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?", - [todoGroupId, userId], - ); + const senderTodoKey = `${todoGroupId}_${userId}`; + const senderTodoData = this.todos.get(senderTodoKey); - if (senderTodo.rows.length === 0) throw new Error("Todo not found"); + if (!senderTodoData) throw new Error("Todo not found"); - const senderTodoRow = senderTodo.rows[0]; const recipientClient = this.users.get(recipientId); const encryptedForRecipient = recipientClient.encrypt( - senderTodoRow.originalText, + senderTodoData.originalText, ); const recipientTodoId = uuidv4(); const now = new Date().toISOString(); // Add recipient as a participant - await this.db.execute( - "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - [todoGroupId, recipientId, now], - ); + participants.add(recipientId); + this.todoParticipants.set(todoGroupId, participants); // Create todo entry for the recipient - await this.db.execute( - "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", - [ - recipientTodoId, - todoGroupId, - recipientId, - JSON.stringify(encryptedForRecipient), - now, - senderTodoRow.originalText, - senderTodoRow.completed, - ], - ); + 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; } diff --git a/validate-build.sh b/validate-build.sh deleted file mode 100755 index 9264a3b..0000000 --- a/validate-build.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash - -echo "🔍 Validating Docker build setup..." - -# Check if required files exist -echo "📁 Checking required files..." -required_files=( - "package.json" - "pnpm-lock.yaml" - "server.js" - "todo-service.js" - "signal-crypto.js" - "public/index.html" - "public/styles.css" - "Dockerfile" - ".dockerignore" -) - -missing_files=() -for file in "${required_files[@]}"; do - if [[ ! -f "$file" ]]; then - missing_files+=("$file") - else - echo "✅ $file" - fi -done - -if [[ ${#missing_files[@]} -gt 0 ]]; then - echo "❌ Missing files:" - printf '%s\n' "${missing_files[@]}" - exit 1 -fi - -# Validate Node.js syntax -echo "" -echo "🔍 Validating JavaScript syntax..." -if command -v node >/dev/null 2>&1; then - for js_file in server.js todo-service.js signal-crypto.js; do - if node -c "$js_file"; then - echo "✅ $js_file syntax OK" - else - echo "❌ $js_file has syntax errors" - exit 1 - fi - done -else - echo "⚠️ Node.js not found, skipping syntax validation" -fi - -# Check package.json for required dependencies -echo "" -echo "📦 Checking package.json dependencies..." -if command -v jq >/dev/null 2>&1; then - required_deps=("express" "uuid" "ws" "dotenv" "@signalapp/libsignal-client") - for dep in "${required_deps[@]}"; do - if jq -e ".dependencies.\"$dep\"" package.json >/dev/null; then - echo "✅ $dep dependency found" - else - echo "❌ Missing dependency: $dep" - exit 1 - fi - done -else - echo "⚠️ jq not found, checking dependencies manually..." - if grep -q '"express"' package.json; then - echo "✅ express dependency found" - else - echo "❌ Missing dependency: express" - exit 1 - fi -fi - -# Validate Dockerfile syntax -echo "" -echo "🐳 Validating Dockerfile..." -if command -v docker >/dev/null 2>&1; then - if docker build --dry-run . >/dev/null 2>&1; then - echo "✅ Dockerfile syntax OK" - else - echo "❌ Dockerfile has issues" - exit 1 - fi -else - echo "⚠️ Docker not found, checking Dockerfile manually..." - if grep -q "FROM node:" Dockerfile; then - echo "✅ Dockerfile has Node.js base image" - else - echo "❌ Dockerfile missing Node.js base image" - exit 1 - fi -fi - -# Check environment variables -echo "" -echo "🔧 Checking environment setup..." -if [[ -f ".env" ]]; then - echo "✅ .env file found" - if grep -q "SQLITE_DB_PATH" .env; then - echo "✅ SQLITE_DB_PATH configured" - else - echo "⚠️ SQLITE_DB_PATH not found in .env" - fi -else - echo "⚠️ .env file not found (will use defaults)" -fi - -# Estimate Docker image size -echo "" -echo "📊 Estimating build efficiency..." -total_size=$(du -sh . 2>/dev/null | cut -f1) -echo "📁 Project size: $total_size" - -if [[ -f ".dockerignore" ]]; then - ignored_items=$(wc -l < .dockerignore) - echo "🚫 .dockerignore rules: $ignored_items" -else - echo "⚠️ No .dockerignore found - build may be slower" -fi - -echo "" -echo "✅ Build validation complete!" -echo "" -echo "🚀 To build the Docker image:" -echo " docker build -t encrypted-todo ." -echo "" -echo "🏃 To run the container:" -echo " docker run -p 3000:3000 -v todo_data:/app/data encrypted-todo" -echo "" -echo "🔗 To use with Docker Compose:" -echo " docker compose up --build"