fix
All checks were successful
Deploy Encrypted Todo App / build-and-push (push) Successful in 2m36s
All checks were successful
Deploy Encrypted Todo App / build-and-push (push) Successful in 2m36s
This commit is contained in:
@ -3,6 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@libsql/client": "^0.15.9",
|
||||||
"@signalapp/libsignal-client": "^0.74.1",
|
"@signalapp/libsignal-client": "^0.74.1",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
232
pnpm-lock.yaml
generated
232
pnpm-lock.yaml
generated
@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@libsql/client':
|
||||||
|
specifier: ^0.15.9
|
||||||
|
version: 0.15.9
|
||||||
'@signalapp/libsignal-client':
|
'@signalapp/libsignal-client':
|
||||||
specifier: ^0.74.1
|
specifier: ^0.74.1
|
||||||
version: 0.74.1
|
version: 0.74.1
|
||||||
@ -26,9 +29,79 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@libsql/client@0.15.9':
|
||||||
|
resolution: {integrity: sha512-VT3do0a0vwYVaNcp/y05ikkKS3OrFR5UeEf5SUuYZVgKVl1Nc1k9ajoYSsOid8AD/vlhLDB5yFQaV4HmT/OB9w==}
|
||||||
|
|
||||||
|
'@libsql/core@0.15.9':
|
||||||
|
resolution: {integrity: sha512-4OVdeAmuaCUq5hYT8NNn0nxlO9AcA/eTjXfUZ+QK8MT3Dz7Z76m73x7KxjU6I64WyXX98dauVH2b9XM+d84npw==}
|
||||||
|
|
||||||
|
'@libsql/darwin-arm64@0.5.13':
|
||||||
|
resolution: {integrity: sha512-ASz/EAMLDLx3oq9PVvZ4zBXXHbz2TxtxUwX2xpTRFR4V4uSHAN07+jpLu3aK5HUBLuv58z7+GjaL5w/cyjR28Q==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@libsql/darwin-x64@0.5.13':
|
||||||
|
resolution: {integrity: sha512-kzglniv1difkq8opusSXM7u9H0WoEPeKxw0ixIfcGfvlCVMJ+t9UNtXmyNHW68ljdllje6a4C6c94iPmIYafYA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@libsql/hrana-client@0.7.0':
|
||||||
|
resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==}
|
||||||
|
|
||||||
|
'@libsql/isomorphic-fetch@0.3.1':
|
||||||
|
resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@libsql/isomorphic-ws@0.1.5':
|
||||||
|
resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==}
|
||||||
|
|
||||||
|
'@libsql/linux-arm-gnueabihf@0.5.13':
|
||||||
|
resolution: {integrity: sha512-UEW+VZN2r0mFkfztKOS7cqfS8IemuekbjUXbXCwULHtusww2QNCXvM5KU9eJCNE419SZCb0qaEWYytcfka8qeA==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/linux-arm-musleabihf@0.5.13':
|
||||||
|
resolution: {integrity: sha512-NMDgLqryYBv4Sr3WoO/m++XDjR5KLlw9r/JK4Ym6A1XBv2bxQQNhH0Lxx3bjLW8qqhBD4+0xfms4d2cOlexPyA==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/linux-arm64-gnu@0.5.13':
|
||||||
|
resolution: {integrity: sha512-/wCxVdrwl1ee6D6LEjwl+w4SxuLm5UL9Kb1LD5n0bBGs0q+49ChdPPh7tp175iRgkcrTgl23emymvt1yj3KxVQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/linux-arm64-musl@0.5.13':
|
||||||
|
resolution: {integrity: sha512-xnVAbZIanUgX57XqeI5sNaDnVilp0Di5syCLSEo+bRyBobe/1IAeehNZpyVbCy91U2N6rH1C/mZU7jicVI9x+A==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/linux-x64-gnu@0.5.13':
|
||||||
|
resolution: {integrity: sha512-/mfMRxcQAI9f8t7tU3QZyh25lXgXKzgin9B9TOSnchD73PWtsVhlyfA6qOCfjQl5kr4sHscdXD5Yb3KIoUgrpQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/linux-x64-musl@0.5.13':
|
||||||
|
resolution: {integrity: sha512-rdefPTpQCVwUjIQYbDLMv3qpd5MdrT0IeD0UZPGqhT9AWU8nJSQoj2lfyIDAWEz7PPOVCY4jHuEn7FS2sw9kRA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/win32-x64-msvc@0.5.13':
|
||||||
|
resolution: {integrity: sha512-aNcmDrD1Ws+dNZIv9ECbxBQumqB9MlSVEykwfXJpqv/593nABb8Ttg5nAGUPtnADyaGDTrGvPPP81d/KsKho4Q==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@neon-rs/load@0.0.4':
|
||||||
|
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
|
||||||
|
|
||||||
'@signalapp/libsignal-client@0.74.1':
|
'@signalapp/libsignal-client@0.74.1':
|
||||||
resolution: {integrity: sha512-PEJou0yrBvxaAGg7JjONlRNM/t3PCBuY96wu7W6+57e38/7Mibo9kAMfE5B8DgVv+DUNMW9AgJhx5McCoIXYew==}
|
resolution: {integrity: sha512-PEJou0yrBvxaAGg7JjONlRNM/t3PCBuY96wu7W6+57e38/7Mibo9kAMfE5B8DgVv+DUNMW9AgJhx5McCoIXYew==}
|
||||||
|
|
||||||
|
'@types/node@24.0.3':
|
||||||
|
resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==}
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -65,6 +138,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
data-uri-to-buffer@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@ -78,6 +155,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
detect-libc@2.0.2:
|
||||||
|
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
dotenv@16.5.0:
|
dotenv@16.5.0:
|
||||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -116,10 +197,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
fetch-blob@3.2.0:
|
||||||
|
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||||
|
engines: {node: ^12.20 || >= 14.13}
|
||||||
|
|
||||||
finalhandler@2.1.0:
|
finalhandler@2.1.0:
|
||||||
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
formdata-polyfill@4.0.10:
|
||||||
|
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -169,6 +258,14 @@ packages:
|
|||||||
is-promise@4.0.0:
|
is-promise@4.0.0:
|
||||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||||
|
|
||||||
|
js-base64@3.7.7:
|
||||||
|
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
||||||
|
|
||||||
|
libsql@0.5.13:
|
||||||
|
resolution: {integrity: sha512-5Bwoa/CqzgkTwySgqHA5TsaUDRrdLIbdM4egdPcaAnqO3aC+qAgS6BwdzuZwARA5digXwiskogZ8H7Yy4XfdOg==}
|
||||||
|
cpu: [x64, arm64, wasm32, arm]
|
||||||
|
os: [darwin, linux, win32]
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -196,6 +293,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
deprecated: Use your platform's native DOMException instead
|
||||||
|
|
||||||
|
node-fetch@3.3.2:
|
||||||
|
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
node-gyp-build@4.8.4:
|
node-gyp-build@4.8.4:
|
||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -219,6 +325,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
|
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
promise-limit@2.7.0:
|
||||||
|
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@ -292,6 +401,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
undici-types@7.8.0:
|
||||||
|
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||||
|
|
||||||
unpipe@1.0.0:
|
unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -304,6 +416,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
web-streams-polyfill@3.3.3:
|
||||||
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
@ -321,12 +437,84 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@libsql/client@0.15.9':
|
||||||
|
dependencies:
|
||||||
|
'@libsql/core': 0.15.9
|
||||||
|
'@libsql/hrana-client': 0.7.0
|
||||||
|
js-base64: 3.7.7
|
||||||
|
libsql: 0.5.13
|
||||||
|
promise-limit: 2.7.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@libsql/core@0.15.9':
|
||||||
|
dependencies:
|
||||||
|
js-base64: 3.7.7
|
||||||
|
|
||||||
|
'@libsql/darwin-arm64@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/darwin-x64@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/hrana-client@0.7.0':
|
||||||
|
dependencies:
|
||||||
|
'@libsql/isomorphic-fetch': 0.3.1
|
||||||
|
'@libsql/isomorphic-ws': 0.1.5
|
||||||
|
js-base64: 3.7.7
|
||||||
|
node-fetch: 3.3.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@libsql/isomorphic-fetch@0.3.1': {}
|
||||||
|
|
||||||
|
'@libsql/isomorphic-ws@0.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/ws': 8.18.1
|
||||||
|
ws: 8.18.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@libsql/linux-arm-gnueabihf@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/linux-arm-musleabihf@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/linux-arm64-gnu@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/linux-arm64-musl@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/linux-x64-gnu@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/linux-x64-musl@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/win32-x64-msvc@0.5.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@neon-rs/load@0.0.4': {}
|
||||||
|
|
||||||
'@signalapp/libsignal-client@0.74.1':
|
'@signalapp/libsignal-client@0.74.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
node-gyp-build: 4.8.4
|
node-gyp-build: 4.8.4
|
||||||
type-fest: 4.41.0
|
type-fest: 4.41.0
|
||||||
uuid: 11.1.0
|
uuid: 11.1.0
|
||||||
|
|
||||||
|
'@types/node@24.0.3':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.8.0
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.0.3
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.1
|
||||||
@ -368,12 +556,16 @@ snapshots:
|
|||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
data-uri-to-buffer@4.0.1: {}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
|
detect-libc@2.0.2: {}
|
||||||
|
|
||||||
dotenv@16.5.0: {}
|
dotenv@16.5.0: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
@ -430,6 +622,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
fetch-blob@3.2.0:
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 3.3.3
|
||||||
|
|
||||||
finalhandler@2.1.0:
|
finalhandler@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
@ -441,6 +638,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
formdata-polyfill@4.0.10:
|
||||||
|
dependencies:
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
@ -491,6 +692,23 @@ snapshots:
|
|||||||
|
|
||||||
is-promise@4.0.0: {}
|
is-promise@4.0.0: {}
|
||||||
|
|
||||||
|
js-base64@3.7.7: {}
|
||||||
|
|
||||||
|
libsql@0.5.13:
|
||||||
|
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
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
media-typer@1.1.0: {}
|
media-typer@1.1.0: {}
|
||||||
@ -507,6 +725,14 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
|
node-fetch@3.3.2:
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer: 4.0.1
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
formdata-polyfill: 4.0.10
|
||||||
|
|
||||||
node-gyp-build@4.8.4: {}
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
@ -523,6 +749,8 @@ snapshots:
|
|||||||
|
|
||||||
path-to-regexp@8.2.0: {}
|
path-to-regexp@8.2.0: {}
|
||||||
|
|
||||||
|
promise-limit@2.7.0: {}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
@ -624,12 +852,16 @@ snapshots:
|
|||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.1
|
||||||
|
|
||||||
|
undici-types@7.8.0: {}
|
||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
uuid@11.1.0: {}
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
|
web-streams-polyfill@3.3.3: {}
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
ws@8.18.2: {}
|
ws@8.18.2: {}
|
||||||
|
257
server.js
257
server.js
@ -31,130 +31,129 @@ wss.on("connection", (ws, req) => {
|
|||||||
connections.delete(userId);
|
connections.delete(userId);
|
||||||
console.log(`User ${userId} disconnected`);
|
console.log(`User ${userId} disconnected`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ws.on("error", (error) => {
|
||||||
|
console.error(`WebSocket error for user ${userId}:`, error);
|
||||||
|
connections.delete(userId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Broadcast to all connected clients
|
// Helper function to broadcast to all connected users
|
||||||
function broadcast(message) {
|
function broadcast(message) {
|
||||||
const messageStr = JSON.stringify(message);
|
const messageStr = JSON.stringify(message);
|
||||||
connections.forEach((ws) => {
|
connections.forEach((ws, userId) => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(messageStr);
|
ws.send(messageStr);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast to specific user
|
// Helper function to broadcast to a specific user
|
||||||
function broadcastToUser(userId, message) {
|
function broadcastToUser(userId, message) {
|
||||||
const ws = connections.get(userId);
|
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) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
console.log(
|
|
||||||
`✓ Sending WebSocket message to ${userId}:`,
|
|
||||||
message.type,
|
|
||||||
JSON.stringify(message.data),
|
|
||||||
);
|
|
||||||
ws.send(JSON.stringify(message));
|
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.json());
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
|
|
||||||
// Create user
|
// Middleware to handle async errors
|
||||||
app.post("/api/users", (req, res) => {
|
const asyncHandler = (fn) => (req, res, next) => {
|
||||||
try {
|
Promise.resolve(fn(req, res, next)).catch(next);
|
||||||
const { userId } = req.body;
|
};
|
||||||
const user = todoService.createUser(userId);
|
|
||||||
broadcast({ type: "users", data: todoService.getAllUsers() });
|
// Error handling middleware
|
||||||
res.json(user);
|
app.use((err, req, res, next) => {
|
||||||
} catch (error) {
|
console.error("Error:", err);
|
||||||
res.status(400).json({ error: error.message });
|
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
|
// Get all users
|
||||||
app.get("/api/users", (req, res) => {
|
app.get(
|
||||||
try {
|
"/api/users",
|
||||||
const users = todoService.getAllUsers();
|
asyncHandler(async (req, res) => {
|
||||||
|
const users = await todoService.getAllUsers();
|
||||||
res.json(users);
|
res.json(users);
|
||||||
} catch (error) {
|
}),
|
||||||
res.status(400).json({ error: error.message });
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add todo
|
// Add a new todo
|
||||||
app.post("/api/users/:userId/todos", (req, res) => {
|
app.post(
|
||||||
try {
|
"/api/users/:userId/todos",
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
const { text } = req.body;
|
const { text } = req.body;
|
||||||
const todoGroupId = todoService.addTodo(userId, text);
|
const todoGroupId = await todoService.addTodo(userId, text);
|
||||||
const todo = todoService.getTodo(userId, todoGroupId);
|
const todo = await todoService.getTodo(userId, todoGroupId);
|
||||||
broadcastToUser(userId, { type: "todo_added", data: todo });
|
broadcastToUser(userId, { type: "todo_added", data: todo });
|
||||||
res.json({ id: todoGroupId });
|
res.json({ id: todoGroupId });
|
||||||
} catch (error) {
|
}),
|
||||||
res.status(400).json({ error: error.message });
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get todos
|
// Get todos for a user
|
||||||
app.get("/api/users/:userId/todos", (req, res) => {
|
app.get(
|
||||||
try {
|
"/api/users/:userId/todos",
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
const todos = todoService.getTodos(userId);
|
const todos = await todoService.getTodos(userId);
|
||||||
res.json(todos);
|
res.json(todos);
|
||||||
} catch (error) {
|
}),
|
||||||
res.status(400).json({ error: error.message });
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get encrypted todos
|
// Get encrypted todos (for debugging)
|
||||||
app.get("/api/users/:userId/todos/encrypted", (req, res) => {
|
app.get(
|
||||||
try {
|
"/api/users/:userId/todos/encrypted",
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
const todos = todoService.getTodos(userId);
|
const todos = await todoService.getTodos(userId);
|
||||||
const encryptedTodos = todos.map((todo) => {
|
const encryptedTodos = [];
|
||||||
const row = todoService.db
|
|
||||||
.prepare(
|
|
||||||
"SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?",
|
|
||||||
)
|
|
||||||
.get(todo.id, userId);
|
|
||||||
|
|
||||||
if (!row) {
|
for (const todo of todos) {
|
||||||
|
const row = await todoService.db.execute(
|
||||||
|
"SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?",
|
||||||
|
[todo.id, userId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (row.rows.length === 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`No encrypted data found for todo ${todo.id} and user ${userId}`,
|
`No encrypted data found for todo ${todo.id} and user ${userId}`,
|
||||||
);
|
);
|
||||||
return {
|
continue;
|
||||||
id: todo.id,
|
|
||||||
encrypted: { body: "Encryption data not available" },
|
|
||||||
createdAt: todo.createdAt,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
encryptedTodos.push({
|
||||||
id: todo.id,
|
id: todo.id,
|
||||||
encrypted: JSON.parse(row.encrypted),
|
encrypted: row.rows[0].encrypted,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.rows[0].createdAt,
|
||||||
};
|
participants: todo.participants,
|
||||||
});
|
});
|
||||||
res.json(encryptedTodos);
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in encrypted todos endpoint:", error);
|
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete todo
|
res.json(encryptedTodos);
|
||||||
app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => {
|
}),
|
||||||
try {
|
);
|
||||||
|
|
||||||
|
// Delete a todo
|
||||||
|
app.delete(
|
||||||
|
"/api/users/:userId/todos/:todoGroupId",
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { userId, todoGroupId } = req.params;
|
const { userId, todoGroupId } = req.params;
|
||||||
const { deletedIds, affectedUsers } = todoService.deleteTodo(
|
const { deletedIds, affectedUsers } = await todoService.deleteTodo(
|
||||||
userId,
|
userId,
|
||||||
todoGroupId,
|
todoGroupId,
|
||||||
);
|
);
|
||||||
@ -163,9 +162,6 @@ app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => {
|
|||||||
console.log(`Broadcasting deletion to users: ${affectedUsers.join(", ")}`);
|
console.log(`Broadcasting deletion to users: ${affectedUsers.join(", ")}`);
|
||||||
affectedUsers.forEach((affectedUserId) => {
|
affectedUsers.forEach((affectedUserId) => {
|
||||||
deletedIds.forEach((deletedId) => {
|
deletedIds.forEach((deletedId) => {
|
||||||
console.log(
|
|
||||||
`Sending todo_deleted event for todo ${deletedId} to user ${affectedUserId}`,
|
|
||||||
);
|
|
||||||
broadcastToUser(affectedUserId, {
|
broadcastToUser(affectedUserId, {
|
||||||
type: "todo_deleted",
|
type: "todo_deleted",
|
||||||
data: { id: deletedId, deletedBy: userId },
|
data: { id: deletedId, deletedBy: userId },
|
||||||
@ -173,43 +169,41 @@ app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ deletedIds, affectedUsers });
|
||||||
} catch (error) {
|
}),
|
||||||
res.status(400).json({ error: error.message });
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Share todo
|
// Share a todo with another user
|
||||||
app.post("/api/users/:userId/todos/:todoGroupId/share", (req, res) => {
|
app.post(
|
||||||
try {
|
"/api/users/:userId/todos/:todoGroupId/share",
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { userId, todoGroupId } = req.params;
|
const { userId, todoGroupId } = req.params;
|
||||||
const { recipientId } = req.body;
|
const { recipientId } = req.body;
|
||||||
if (!recipientId) {
|
if (!recipientId) {
|
||||||
return res.status(400).json({ error: "Recipient ID is required" });
|
return res.status(400).json({ error: "Recipient ID is required" });
|
||||||
}
|
}
|
||||||
const sharedTodoGroupId = todoService.shareTodo(
|
const sharedTodoGroupId = await todoService.shareTodo(
|
||||||
userId,
|
userId,
|
||||||
todoGroupId,
|
todoGroupId,
|
||||||
recipientId,
|
recipientId,
|
||||||
);
|
);
|
||||||
const sharedTodo = todoService.getTodo(recipientId, sharedTodoGroupId);
|
const sharedTodo = await todoService.getTodo(
|
||||||
|
recipientId,
|
||||||
|
sharedTodoGroupId,
|
||||||
|
);
|
||||||
broadcastToUser(recipientId, { type: "todo_shared", data: sharedTodo });
|
broadcastToUser(recipientId, { type: "todo_shared", data: sharedTodo });
|
||||||
|
|
||||||
// Also notify the original user that the todo was shared
|
res.json({
|
||||||
broadcastToUser(userId, {
|
sharedTodoGroupId,
|
||||||
type: "todo_share_confirmed",
|
message: `Todo shared with ${recipientId}`,
|
||||||
data: { todoGroupId, sharedWith: recipientId },
|
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
res.json({ success: true });
|
);
|
||||||
} catch (error) {
|
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update todo completion status
|
// Update todo completion status
|
||||||
app.patch("/api/users/:userId/todos/:todoGroupId", (req, res) => {
|
app.patch(
|
||||||
try {
|
"/api/users/:userId/todos/:todoGroupId",
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { userId, todoGroupId } = req.params;
|
const { userId, todoGroupId } = req.params;
|
||||||
const { completed } = req.body;
|
const { completed } = req.body;
|
||||||
|
|
||||||
@ -217,41 +211,38 @@ app.patch("/api/users/:userId/todos/:todoGroupId", (req, res) => {
|
|||||||
`PATCH request: user ${userId} updating todo ${todoGroupId} to completed=${completed}`,
|
`PATCH request: user ${userId} updating todo ${todoGroupId} to completed=${completed}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const affectedUsers = todoService.updateTodoStatus(
|
const affectedUsers = await todoService.updateTodoStatus(
|
||||||
userId,
|
userId,
|
||||||
todoGroupId,
|
todoGroupId,
|
||||||
completed,
|
completed,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`updateTodoStatus returned affected users:`, affectedUsers);
|
console.log(`Broadcasting update to users: ${affectedUsers.join(", ")}`);
|
||||||
|
|
||||||
// Broadcast update to all affected users
|
// Broadcast the update to all affected users
|
||||||
console.log(
|
for (const affectedUserId of affectedUsers) {
|
||||||
`Broadcasting todo update to users: ${affectedUsers.join(", ")}`,
|
try {
|
||||||
);
|
const todo = await todoService.getTodo(affectedUserId, todoGroupId);
|
||||||
affectedUsers.forEach((affectedUserId) => {
|
const message = {
|
||||||
console.log(
|
type: "todo_updated",
|
||||||
`Sending todo_updated event for todo ${todoGroupId} to user ${affectedUserId}`,
|
data: { id: todoGroupId, completed, updatedBy: userId },
|
||||||
);
|
};
|
||||||
const message = {
|
broadcastToUser(affectedUserId, message);
|
||||||
type: "todo_updated",
|
console.log(`Sent update to user ${affectedUserId}:`, message);
|
||||||
data: { id: todoGroupId, completed, updatedBy: userId },
|
} catch (error) {
|
||||||
};
|
console.error(
|
||||||
console.log(
|
`Error getting todo for user ${affectedUserId}:`,
|
||||||
`Message being sent to ${affectedUserId}:`,
|
error.message,
|
||||||
JSON.stringify(message),
|
);
|
||||||
);
|
}
|
||||||
broadcastToUser(affectedUserId, message);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ message: "Todo updated successfully", affectedUsers });
|
||||||
} catch (error) {
|
}),
|
||||||
console.error(`Error updating todo status:`, error);
|
);
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
const PORT = process.env.APP_PORT || 3000;
|
||||||
});
|
|
||||||
|
|
||||||
const PORT = 3000;
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`Server running on port 3000`);
|
console.log(`Server running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
394
todo-service.js
394
todo-service.js
@ -1,18 +1,28 @@
|
|||||||
import { DatabaseSync } from "node:sqlite";
|
import { createClient } from "@libsql/client";
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { SignalClient } from "./signal-crypto.js";
|
import { SignalClient } from "./signal-crypto.js";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
export class EncryptedTodoService {
|
export class EncryptedTodoService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.db = new DatabaseSync(process.env.SQLITE_DB_PATH);
|
this.db = createClient({
|
||||||
|
url: `file:${process.env.SQLITE_DB_PATH}`,
|
||||||
|
});
|
||||||
this.users = new Map();
|
this.users = new Map();
|
||||||
|
|
||||||
|
// Initialize database and migrate
|
||||||
|
this.initializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeDatabase() {
|
||||||
// Create tables with new schema
|
// Create tables with new schema
|
||||||
this.db.exec(`
|
await this.db.execute(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
userId TEXT PRIMARY KEY
|
userId TEXT PRIMARY KEY
|
||||||
);
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await this.db.execute(`
|
||||||
CREATE TABLE IF NOT EXISTS todos (
|
CREATE TABLE IF NOT EXISTS todos (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
todoGroupId TEXT,
|
todoGroupId TEXT,
|
||||||
@ -25,12 +35,18 @@ export class EncryptedTodoService {
|
|||||||
originalTodoId TEXT,
|
originalTodoId TEXT,
|
||||||
FOREIGN KEY(userId) REFERENCES users(userId)
|
FOREIGN KEY(userId) REFERENCES users(userId)
|
||||||
);
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await this.db.execute(`
|
||||||
CREATE TABLE IF NOT EXISTS todo_groups (
|
CREATE TABLE IF NOT EXISTS todo_groups (
|
||||||
todoGroupId TEXT PRIMARY KEY,
|
todoGroupId TEXT PRIMARY KEY,
|
||||||
createdBy TEXT,
|
createdBy TEXT,
|
||||||
createdAt TEXT,
|
createdAt TEXT,
|
||||||
FOREIGN KEY(createdBy) REFERENCES users(userId)
|
FOREIGN KEY(createdBy) REFERENCES users(userId)
|
||||||
);
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await this.db.execute(`
|
||||||
CREATE TABLE IF NOT EXISTS todo_participants (
|
CREATE TABLE IF NOT EXISTS todo_participants (
|
||||||
todoGroupId TEXT,
|
todoGroupId TEXT,
|
||||||
userId TEXT,
|
userId TEXT,
|
||||||
@ -39,6 +55,9 @@ export class EncryptedTodoService {
|
|||||||
FOREIGN KEY(todoGroupId) REFERENCES todo_groups(todoGroupId),
|
FOREIGN KEY(todoGroupId) REFERENCES todo_groups(todoGroupId),
|
||||||
FOREIGN KEY(userId) REFERENCES users(userId)
|
FOREIGN KEY(userId) REFERENCES users(userId)
|
||||||
);
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await this.db.execute(`
|
||||||
CREATE TABLE IF NOT EXISTS todo_shares (
|
CREATE TABLE IF NOT EXISTS todo_shares (
|
||||||
todoId TEXT,
|
todoId TEXT,
|
||||||
sharedWith TEXT,
|
sharedWith TEXT,
|
||||||
@ -48,84 +67,90 @@ export class EncryptedTodoService {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
// Migrate existing data to new schema
|
// Migrate existing data to new schema
|
||||||
this.migrateToNewSchema();
|
await this.migrateToNewSchema();
|
||||||
|
|
||||||
// Load existing users from database on startup
|
// Load existing users from database on startup
|
||||||
this.loadExistingUsers();
|
await this.loadExistingUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
migrateToNewSchema() {
|
async migrateToNewSchema() {
|
||||||
// Check if we need to migrate by looking for todos without todoGroupId
|
// Check if we need to migrate by looking for todos without todoGroupId
|
||||||
const oldTodos = this.db
|
const oldTodos = await this.db.execute(
|
||||||
.prepare("SELECT * FROM todos WHERE todoGroupId IS NULL")
|
"SELECT * FROM todos WHERE todoGroupId IS NULL",
|
||||||
.all();
|
);
|
||||||
|
|
||||||
if (oldTodos.length > 0) {
|
if (oldTodos.rows.length > 0) {
|
||||||
console.log(`Migrating ${oldTodos.length} todos to new schema...`);
|
console.log(`Migrating ${oldTodos.rows.length} todos to new schema...`);
|
||||||
|
|
||||||
for (const todo of oldTodos) {
|
for (const todo of oldTodos.rows) {
|
||||||
const todoGroupId = uuidv4();
|
const todoGroupId = uuidv4();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
// If this is an original todo (not shared)
|
// If this is an original todo (not shared)
|
||||||
if (!todo.sharedBy && !todo.originalTodoId) {
|
if (!todo.sharedBy && !todo.originalTodoId) {
|
||||||
// Create todo group
|
// Create todo group
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT OR IGNORE INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)",
|
||||||
"INSERT OR IGNORE INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)",
|
[todoGroupId, todo.userId, todo.createdAt || now],
|
||||||
)
|
);
|
||||||
.run(todoGroupId, todo.userId, todo.createdAt || now);
|
|
||||||
|
|
||||||
// Add creator as participant
|
// Add creator as participant
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
||||||
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
[todoGroupId, todo.userId, todo.createdAt || now],
|
||||||
)
|
);
|
||||||
.run(todoGroupId, todo.userId, todo.createdAt || now);
|
|
||||||
|
|
||||||
// Update the todo with todoGroupId
|
// Update the todo with todoGroupId
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?")
|
"UPDATE todos SET todoGroupId = ? WHERE id = ?",
|
||||||
.run(todoGroupId, todo.id);
|
[todoGroupId, todo.id],
|
||||||
|
);
|
||||||
|
|
||||||
// Find and migrate shared copies
|
// Find and migrate shared copies
|
||||||
const sharedCopies = this.db
|
const sharedCopies = await this.db.execute(
|
||||||
.prepare("SELECT * FROM todos WHERE originalTodoId = ?")
|
"SELECT * FROM todos WHERE originalTodoId = ?",
|
||||||
.all(todo.id);
|
[todo.id],
|
||||||
for (const sharedTodo of sharedCopies) {
|
);
|
||||||
|
|
||||||
|
for (const sharedTodo of sharedCopies.rows) {
|
||||||
// Add shared user as participant
|
// Add shared user as participant
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
||||||
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
[todoGroupId, sharedTodo.userId, sharedTodo.createdAt || now],
|
||||||
)
|
);
|
||||||
.run(todoGroupId, sharedTodo.userId, sharedTodo.createdAt || now);
|
|
||||||
|
|
||||||
// Update shared todo with same todoGroupId
|
// Update shared todo with same todoGroupId
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?")
|
"UPDATE todos SET todoGroupId = ? WHERE id = ?",
|
||||||
.run(todoGroupId, sharedTodo.id);
|
[todoGroupId, sharedTodo.id],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If this is a shared todo without a group (shouldn't happen after above, but just in case)
|
// If this is a shared todo without a group (shouldn't happen after above, but just in case)
|
||||||
else if (todo.originalTodoId && !todo.todoGroupId) {
|
else if (todo.originalTodoId && !todo.todoGroupId) {
|
||||||
const originalTodo = this.db
|
const originalTodo = await this.db.execute(
|
||||||
.prepare("SELECT todoGroupId FROM todos WHERE id = ?")
|
"SELECT todoGroupId FROM todos WHERE id = ?",
|
||||||
.get(todo.originalTodoId);
|
[todo.originalTodoId],
|
||||||
if (originalTodo && originalTodo.todoGroupId) {
|
);
|
||||||
this.db
|
|
||||||
.prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?")
|
if (
|
||||||
.run(originalTodo.todoGroupId, todo.id);
|
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
|
// Add as participant if not already
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
||||||
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
[
|
||||||
)
|
originalTodo.rows[0].todoGroupId,
|
||||||
.run(
|
|
||||||
originalTodo.todoGroupId,
|
|
||||||
todo.userId,
|
todo.userId,
|
||||||
todo.createdAt || now,
|
todo.createdAt || now,
|
||||||
);
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,9 +159,9 @@ export class EncryptedTodoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadExistingUsers() {
|
async loadExistingUsers() {
|
||||||
const existingUsers = this.db.prepare("SELECT userId FROM users").all();
|
const existingUsers = await this.db.execute("SELECT userId FROM users");
|
||||||
for (const user of existingUsers) {
|
for (const user of existingUsers.rows) {
|
||||||
if (!this.users.has(user.userId)) {
|
if (!this.users.has(user.userId)) {
|
||||||
this.users.set(user.userId, SignalClient.create(user.userId));
|
this.users.set(user.userId, SignalClient.create(user.userId));
|
||||||
console.log(`Loaded existing user: ${user.userId}`);
|
console.log(`Loaded existing user: ${user.userId}`);
|
||||||
@ -144,24 +169,22 @@ export class EncryptedTodoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createUser(userId) {
|
async createUser(userId) {
|
||||||
this.db
|
await this.db.execute("INSERT OR IGNORE INTO users (userId) VALUES (?)", [
|
||||||
.prepare("INSERT OR IGNORE INTO users (userId) VALUES (?)")
|
userId,
|
||||||
.run(userId);
|
]);
|
||||||
if (!this.users.has(userId)) {
|
if (!this.users.has(userId)) {
|
||||||
this.users.set(userId, SignalClient.create(userId));
|
this.users.set(userId, SignalClient.create(userId));
|
||||||
}
|
}
|
||||||
return { userId };
|
return { userId };
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllUsers() {
|
async getAllUsers() {
|
||||||
return this.db
|
const result = await this.db.execute("SELECT userId FROM users");
|
||||||
.prepare("SELECT userId FROM users")
|
return result.rows.map((u) => u.userId);
|
||||||
.all()
|
|
||||||
.map((u) => u.userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addTodo(userId, todoText) {
|
async addTodo(userId, todoText) {
|
||||||
const todoGroupId = uuidv4();
|
const todoGroupId = uuidv4();
|
||||||
const client = this.users.get(userId);
|
const client = this.users.get(userId);
|
||||||
const todoId = uuidv4();
|
const todoId = uuidv4();
|
||||||
@ -169,25 +192,21 @@ export class EncryptedTodoService {
|
|||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
// Create the todo group
|
// Create the todo group
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)",
|
||||||
"INSERT INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)",
|
[todoGroupId, userId, now],
|
||||||
)
|
);
|
||||||
.run(todoGroupId, userId, now);
|
|
||||||
|
|
||||||
// Add the creator as a participant
|
// Add the creator as a participant
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
||||||
"INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
[todoGroupId, userId, now],
|
||||||
)
|
);
|
||||||
.run(todoGroupId, userId, now);
|
|
||||||
|
|
||||||
// Create the todo entry for the creator
|
// Create the todo entry for the creator
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
"INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
[
|
||||||
)
|
|
||||||
.run(
|
|
||||||
todoId,
|
todoId,
|
||||||
todoGroupId,
|
todoGroupId,
|
||||||
userId,
|
userId,
|
||||||
@ -195,24 +214,25 @@ export class EncryptedTodoService {
|
|||||||
now,
|
now,
|
||||||
todoText,
|
todoText,
|
||||||
0,
|
0,
|
||||||
);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return todoGroupId;
|
return todoGroupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTodos(userId) {
|
async getTodos(userId) {
|
||||||
const client = this.users.get(userId);
|
const client = this.users.get(userId);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.error(`No client found for user ${userId}`);
|
console.error(`No client found for user ${userId}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = this.db
|
const rows = await this.db.execute("SELECT * FROM todos WHERE userId = ?", [
|
||||||
.prepare("SELECT * FROM todos WHERE userId = ?")
|
userId,
|
||||||
.all(userId);
|
]);
|
||||||
const todos = [];
|
const todos = [];
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows.rows) {
|
||||||
try {
|
try {
|
||||||
// Skip todos without todoGroupId (migration didn't work)
|
// Skip todos without todoGroupId (migration didn't work)
|
||||||
if (!row.todoGroupId) {
|
if (!row.todoGroupId) {
|
||||||
@ -223,24 +243,28 @@ export class EncryptedTodoService {
|
|||||||
const decryptedText = client.decrypt(JSON.parse(row.encrypted));
|
const decryptedText = client.decrypt(JSON.parse(row.encrypted));
|
||||||
|
|
||||||
// Get all participants in this todo group
|
// Get all participants in this todo group
|
||||||
const participants = this.db
|
const participants = await this.db.execute(
|
||||||
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?")
|
"SELECT userId FROM todo_participants WHERE todoGroupId = ?",
|
||||||
.all(row.todoGroupId)
|
[row.todoGroupId],
|
||||||
.map((p) => p.userId);
|
);
|
||||||
|
|
||||||
// Get group creator
|
// Get group creator
|
||||||
const groupInfo = this.db
|
const groupInfo = await this.db.execute(
|
||||||
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?")
|
"SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
|
||||||
.get(row.todoGroupId);
|
[row.todoGroupId],
|
||||||
|
);
|
||||||
|
|
||||||
todos.push({
|
todos.push({
|
||||||
id: row.todoGroupId, // Use todoGroupId as the primary identifier
|
id: row.todoGroupId, // Use todoGroupId as the primary identifier
|
||||||
text: decryptedText,
|
text: decryptedText,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
completed: !!row.completed,
|
completed: !!row.completed,
|
||||||
participants: participants.length > 0 ? participants : [userId], // Fallback to current user
|
participants:
|
||||||
createdBy: groupInfo?.createdBy || userId, // Fallback to current user
|
participants.rows.length > 0
|
||||||
isCreator: (groupInfo?.createdBy || userId) === userId,
|
? 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,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error processing todo ${row.id}:`, e);
|
console.error(`Error processing todo ${row.id}:`, e);
|
||||||
@ -249,183 +273,197 @@ export class EncryptedTodoService {
|
|||||||
return todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
return todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTodo(userId, todoGroupId) {
|
async getTodo(userId, todoGroupId) {
|
||||||
const client = this.users.get(userId);
|
const client = this.users.get(userId);
|
||||||
const row = this.db
|
const row = await this.db.execute(
|
||||||
.prepare("SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?")
|
"SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?",
|
||||||
.get(todoGroupId, userId);
|
[todoGroupId, userId],
|
||||||
if (!row) throw new Error("Todo not found");
|
);
|
||||||
|
|
||||||
const decryptedText = client.decrypt(JSON.parse(row.encrypted));
|
if (row.rows.length === 0) 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
|
// Get all participants in this todo group
|
||||||
const participants = this.db
|
const participants = await this.db.execute(
|
||||||
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?")
|
"SELECT userId FROM todo_participants WHERE todoGroupId = ?",
|
||||||
.all(todoGroupId)
|
[todoGroupId],
|
||||||
.map((p) => p.userId);
|
);
|
||||||
|
|
||||||
// Get group creator
|
// Get group creator
|
||||||
const groupInfo = this.db
|
const groupInfo = await this.db.execute(
|
||||||
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?")
|
"SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
|
||||||
.get(todoGroupId);
|
[todoGroupId],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: todoGroupId,
|
id: todoGroupId,
|
||||||
text: decryptedText,
|
text: decryptedText,
|
||||||
createdAt: row.createdAt,
|
createdAt: todoRow.createdAt,
|
||||||
completed: !!row.completed,
|
completed: !!todoRow.completed,
|
||||||
participants: participants,
|
participants: participants.rows.map((p) => p.userId),
|
||||||
createdBy: groupInfo?.createdBy,
|
createdBy: groupInfo.rows[0]?.createdBy,
|
||||||
isCreator: groupInfo?.createdBy === userId,
|
isCreator: groupInfo.rows[0]?.createdBy === userId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTodoStatus(userId, todoGroupId, completed) {
|
async updateTodoStatus(userId, todoGroupId, completed) {
|
||||||
console.log(
|
console.log(
|
||||||
`updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`,
|
`updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all participants in this todo group
|
// Get all participants in this todo group
|
||||||
const participants = this.db
|
const participants = await this.db.execute(
|
||||||
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?")
|
"SELECT userId FROM todo_participants WHERE todoGroupId = ?",
|
||||||
.all(todoGroupId)
|
[todoGroupId],
|
||||||
.map((p) => p.userId);
|
);
|
||||||
|
|
||||||
if (!participants.includes(userId)) {
|
const participantIds = participants.rows.map((p) => p.userId);
|
||||||
|
|
||||||
|
if (!participantIds.includes(userId)) {
|
||||||
throw new Error("User is not a participant in this todo group");
|
throw new Error("User is not a participant in this todo group");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Updating todo for all participants:`, participants);
|
console.log(`Updating todo for all participants:`, participantIds);
|
||||||
|
|
||||||
// Update todos for all participants
|
// Update todos for all participants
|
||||||
for (const participantId of participants) {
|
for (const participantId of participantIds) {
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"UPDATE todos SET completed = ? WHERE todoGroupId = ? AND userId = ?",
|
||||||
"UPDATE todos SET completed = ? WHERE todoGroupId = ? AND userId = ?",
|
[completed ? 1 : 0, todoGroupId, participantId],
|
||||||
)
|
);
|
||||||
.run(completed ? 1 : 0, todoGroupId, participantId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Affected users for todo update:`, participants);
|
console.log(`Affected users for todo update:`, participantIds);
|
||||||
return participants;
|
return participantIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTodo(userId, todoGroupId) {
|
async deleteTodo(userId, todoGroupId) {
|
||||||
// Get all participants in this todo group
|
// Get all participants in this todo group
|
||||||
const participants = this.db
|
const participants = await this.db.execute(
|
||||||
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?")
|
"SELECT userId FROM todo_participants WHERE todoGroupId = ?",
|
||||||
.all(todoGroupId)
|
[todoGroupId],
|
||||||
.map((p) => p.userId);
|
);
|
||||||
|
|
||||||
if (!participants.includes(userId)) {
|
const participantIds = participants.rows.map((p) => p.userId);
|
||||||
|
|
||||||
|
if (!participantIds.includes(userId)) {
|
||||||
throw new Error("User is not a participant in this todo group");
|
throw new Error("User is not a participant in this todo group");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is the creator of this todo group
|
// Check if user is the creator of this todo group
|
||||||
const groupInfo = this.db
|
const groupInfo = await this.db.execute(
|
||||||
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?")
|
"SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
|
||||||
.get(todoGroupId);
|
[todoGroupId],
|
||||||
|
);
|
||||||
|
|
||||||
if (!groupInfo) {
|
if (groupInfo.rows.length === 0) {
|
||||||
throw new Error("Todo group not found");
|
throw new Error("Todo group not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupInfo.createdBy !== userId) {
|
if (groupInfo.rows[0].createdBy !== userId) {
|
||||||
throw new Error("Only the creator can delete this todo");
|
throw new Error("Only the creator can delete this todo");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Deleting todo group ${todoGroupId} for all participants:`,
|
`Deleting todo group ${todoGroupId} for all participants:`,
|
||||||
participants,
|
participantIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete all todo entries for this group
|
// Delete all todo entries for this group
|
||||||
this.db.prepare("DELETE FROM todos WHERE todoGroupId = ?").run(todoGroupId);
|
await this.db.execute("DELETE FROM todos WHERE todoGroupId = ?", [
|
||||||
|
todoGroupId,
|
||||||
|
]);
|
||||||
|
|
||||||
// Delete all participants
|
// Delete all participants
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare("DELETE FROM todo_participants WHERE todoGroupId = ?")
|
"DELETE FROM todo_participants WHERE todoGroupId = ?",
|
||||||
.run(todoGroupId);
|
[todoGroupId],
|
||||||
|
);
|
||||||
|
|
||||||
// Delete the group itself
|
// Delete the group itself
|
||||||
this.db
|
await this.db.execute("DELETE FROM todo_groups WHERE todoGroupId = ?", [
|
||||||
.prepare("DELETE FROM todo_groups WHERE todoGroupId = ?")
|
todoGroupId,
|
||||||
.run(todoGroupId);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletedIds: [todoGroupId],
|
deletedIds: [todoGroupId],
|
||||||
affectedUsers: participants,
|
affectedUsers: participantIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
shareTodo(userId, todoGroupId, recipientId) {
|
async shareTodo(userId, todoGroupId, recipientId) {
|
||||||
// Ensure recipient user exists
|
// Ensure recipient user exists
|
||||||
if (!this.users.has(recipientId)) {
|
if (!this.users.has(recipientId)) {
|
||||||
this.users.set(recipientId, SignalClient.create(recipientId));
|
this.users.set(recipientId, SignalClient.create(recipientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is a participant in this todo group
|
// Check if user is a participant in this todo group
|
||||||
const participants = this.db
|
const participants = await this.db.execute(
|
||||||
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?")
|
"SELECT userId FROM todo_participants WHERE todoGroupId = ?",
|
||||||
.all(todoGroupId)
|
[todoGroupId],
|
||||||
.map((p) => p.userId);
|
);
|
||||||
|
|
||||||
if (!participants.includes(userId)) {
|
const participantIds = participants.rows.map((p) => p.userId);
|
||||||
|
|
||||||
|
if (!participantIds.includes(userId)) {
|
||||||
throw new Error("User is not a participant in this todo group");
|
throw new Error("User is not a participant in this todo group");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is the creator of this todo group
|
// Check if user is the creator of this todo group
|
||||||
const groupInfo = this.db
|
const groupInfo = await this.db.execute(
|
||||||
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?")
|
"SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
|
||||||
.get(todoGroupId);
|
[todoGroupId],
|
||||||
|
);
|
||||||
|
|
||||||
if (!groupInfo) {
|
if (groupInfo.rows.length === 0) {
|
||||||
throw new Error("Todo group not found");
|
throw new Error("Todo group not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupInfo.createdBy !== userId) {
|
if (groupInfo.rows[0].createdBy !== userId) {
|
||||||
throw new Error("Only the creator can add participants to this todo");
|
throw new Error("Only the creator can add participants to this todo");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (participants.includes(recipientId)) {
|
if (participantIds.includes(recipientId)) {
|
||||||
throw new Error("User is already a participant in this todo group");
|
throw new Error("User is already a participant in this todo group");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the original text from the sender's todo
|
// Get the original text from the sender's todo
|
||||||
const senderTodo = this.db
|
const senderTodo = await this.db.execute(
|
||||||
.prepare("SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?")
|
"SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?",
|
||||||
.get(todoGroupId, userId);
|
[todoGroupId, userId],
|
||||||
|
);
|
||||||
|
|
||||||
if (!senderTodo) throw new Error("Todo not found");
|
if (senderTodo.rows.length === 0) throw new Error("Todo not found");
|
||||||
|
|
||||||
|
const senderTodoRow = senderTodo.rows[0];
|
||||||
const recipientClient = this.users.get(recipientId);
|
const recipientClient = this.users.get(recipientId);
|
||||||
const encryptedForRecipient = recipientClient.encrypt(
|
const encryptedForRecipient = recipientClient.encrypt(
|
||||||
senderTodo.originalText,
|
senderTodoRow.originalText,
|
||||||
);
|
);
|
||||||
const recipientTodoId = uuidv4();
|
const recipientTodoId = uuidv4();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
// Add recipient as a participant
|
// Add recipient as a participant
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
||||||
"INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
|
[todoGroupId, recipientId, now],
|
||||||
)
|
);
|
||||||
.run(todoGroupId, recipientId, now);
|
|
||||||
|
|
||||||
// Create todo entry for the recipient
|
// Create todo entry for the recipient
|
||||||
this.db
|
await this.db.execute(
|
||||||
.prepare(
|
"INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
"INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
[
|
||||||
)
|
|
||||||
.run(
|
|
||||||
recipientTodoId,
|
recipientTodoId,
|
||||||
todoGroupId,
|
todoGroupId,
|
||||||
recipientId,
|
recipientId,
|
||||||
JSON.stringify(encryptedForRecipient),
|
JSON.stringify(encryptedForRecipient),
|
||||||
now,
|
now,
|
||||||
senderTodo.originalText,
|
senderTodoRow.originalText,
|
||||||
senderTodo.completed,
|
senderTodoRow.completed,
|
||||||
);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return todoGroupId;
|
return todoGroupId;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user