Permissions issues on my deployment server... so I guess in-memory it
Some checks are pending
Deploy Encrypted Todo App / build-and-push (push) Has started running

is!
This commit is contained in:
2025-06-16 10:55:14 -06:00
parent 7f4bb14b18
commit 81d8091354
7 changed files with 122 additions and 830 deletions

View File

@ -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"]

View File

@ -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

345
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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,
});
}

View File

@ -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,
const todoKey = `${todoGroupId}_${userId}`;
this.todos.set(todoKey, {
id: todoId,
todoGroupId,
userId,
JSON.stringify(encryptedTodo),
now,
todoText,
0,
],
);
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,
const recipientTodoKey = `${todoGroupId}_${recipientId}`;
this.todos.set(recipientTodoKey, {
id: recipientTodoId,
todoGroupId,
recipientId,
JSON.stringify(encryptedForRecipient),
now,
senderTodoRow.originalText,
senderTodoRow.completed,
],
);
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;
}

View File

@ -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"