Compare commits
14 Commits
master
...
upload-ima
Author | SHA1 | Date | |
---|---|---|---|
|
d5b2b52f95 | ||
|
654aa3e9c4 | ||
|
3b08d84b26 | ||
|
a675de3420 | ||
|
f60dc3b018 | ||
|
bbc92dc01c | ||
|
35bcc5c5b9 | ||
|
146e1848bc | ||
|
86d8771303 | ||
|
433394a3c2 | ||
|
6de10f07e6 | ||
|
a797aa00e9 | ||
|
a2569a2b34 | ||
|
e9956486fd |
5
.gitignore
vendored
5
.gitignore
vendored
@ -13,7 +13,4 @@ server/.env
|
||||
|
||||
# production configuration
|
||||
docker-compose.server.yml
|
||||
/letsencrypt
|
||||
|
||||
# Dev env configuration
|
||||
proxy.js
|
||||
/letsencrypt
|
@ -12,9 +12,6 @@ The preferred way to report bugs or request new features for the web app or the
|
||||
|
||||
If you want a more interactive way to discuss bugs or features, you can join the [Discord server](https://discord.gg/y3HqyGeABK).
|
||||
|
||||
## Funding
|
||||
|
||||
By popular request I have written a post about how the public instance of Noteshare is funded: https://noteshare.space/funding
|
||||
|
||||
## Local development
|
||||
|
||||
|
@ -52,7 +52,7 @@ services:
|
||||
labels:
|
||||
- "traefik.enable=true" # tell Traefik this is something we would like to expose
|
||||
- "traefik.http.routers.backend.entrypoints=web" # what entrypoint should be used for the backend service.
|
||||
- "traefik.http.routers.backend.rule=Host(`localhost`) && PathPrefix(`/api`) && (Method(`POST`) || Method(`DELETE`))" #
|
||||
- "traefik.http.routers.backend.rule=Host(`localhost`) && PathPrefix(`/api`) && Method(`POST`)" #
|
||||
|
||||
# Frontend for serving encrypted notes over HTML (SvelteKit)
|
||||
frontend:
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"scripts": {
|
||||
"proxy": "node proxy.js",
|
||||
"dev": "concurrently -n plugin,webapp,server,proxy \"npm --prefix ./plugin run dev\" \"npm --prefix ./webapp run dev\" \"npm --prefix ./server run dev\" \"npm run proxy\""
|
||||
"dev": "concurrently -n plugin,webapp,server,proxy \"npm --prefix ./plugin run dev\" \"npm --prefix ./webapp run dev -- --open\" \"npm --prefix ./server run dev\" \"npm run proxy\""
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^7.2.2"
|
||||
|
2
plugin
2
plugin
@ -1 +1 @@
|
||||
Subproject commit 7a645c3e11c14f222405068c4710d30686800013
|
||||
Subproject commit 8ccf08c4d2d1fb99f38488085c3f40c22393c9c0
|
707
pnpm-lock.yaml
generated
707
pnpm-lock.yaml
generated
@ -1,707 +0,0 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
concurrently: ^7.2.2
|
||||
express: ^4.18.1
|
||||
http-proxy-middleware: ^2.0.6
|
||||
|
||||
dependencies:
|
||||
concurrently: 7.5.0
|
||||
|
||||
devDependencies:
|
||||
express: 4.18.2
|
||||
http-proxy-middleware: 2.0.6
|
||||
|
||||
packages:
|
||||
|
||||
/@types/http-proxy/1.17.9:
|
||||
resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==}
|
||||
dependencies:
|
||||
'@types/node': 18.11.9
|
||||
dev: true
|
||||
|
||||
/@types/node/18.11.9:
|
||||
resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
|
||||
dev: true
|
||||
|
||||
/accepts/1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
negotiator: 0.6.3
|
||||
dev: true
|
||||
|
||||
/ansi-regex/5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/ansi-styles/4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
dev: false
|
||||
|
||||
/array-flatten/1.1.1:
|
||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||
dev: true
|
||||
|
||||
/body-parser/1.20.1:
|
||||
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.4
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.4.24
|
||||
on-finished: 2.4.1
|
||||
qs: 6.11.0
|
||||
raw-body: 2.5.1
|
||||
type-is: 1.6.18
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/braces/3.0.2:
|
||||
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
fill-range: 7.0.1
|
||||
dev: true
|
||||
|
||||
/bytes/3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/call-bind/1.0.2:
|
||||
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
get-intrinsic: 1.1.3
|
||||
dev: true
|
||||
|
||||
/chalk/4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
dev: false
|
||||
|
||||
/cliui/8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
dev: false
|
||||
|
||||
/color-convert/2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
dev: false
|
||||
|
||||
/color-name/1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
dev: false
|
||||
|
||||
/concurrently/7.5.0:
|
||||
resolution: {integrity: sha512-5E3mwiS+i2JYBzr5BpXkFxOnleZTMsG+WnE/dCG4/P+oiVXrbmrBwJ2ozn4SxwB2EZDrKR568X+puVohxz3/Mg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
date-fns: 2.29.3
|
||||
lodash: 4.17.21
|
||||
rxjs: 7.5.7
|
||||
shell-quote: 1.7.4
|
||||
spawn-command: 0.0.2-1
|
||||
supports-color: 8.1.1
|
||||
tree-kill: 1.2.2
|
||||
yargs: 17.6.2
|
||||
dev: false
|
||||
|
||||
/content-disposition/0.5.4:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/content-type/1.0.4:
|
||||
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/cookie-signature/1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
dev: true
|
||||
|
||||
/cookie/0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/date-fns/2.29.3:
|
||||
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
||||
engines: {node: '>=0.11'}
|
||||
dev: false
|
||||
|
||||
/debug/2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
dev: true
|
||||
|
||||
/depd/2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/destroy/1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dev: true
|
||||
|
||||
/ee-first/1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
dev: true
|
||||
|
||||
/emoji-regex/8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
dev: false
|
||||
|
||||
/encodeurl/1.0.2:
|
||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/escalade/3.1.1:
|
||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/escape-html/1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
dev: true
|
||||
|
||||
/etag/1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/eventemitter3/4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
dev: true
|
||||
|
||||
/express/4.18.2:
|
||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
array-flatten: 1.1.1
|
||||
body-parser: 1.20.1
|
||||
content-disposition: 0.5.4
|
||||
content-type: 1.0.4
|
||||
cookie: 0.5.0
|
||||
cookie-signature: 1.0.6
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 1.2.0
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.0
|
||||
merge-descriptors: 1.0.1
|
||||
methods: 1.1.2
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 0.1.7
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.11.0
|
||||
range-parser: 1.2.1
|
||||
safe-buffer: 5.2.1
|
||||
send: 0.18.0
|
||||
serve-static: 1.15.0
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.1
|
||||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/fill-range/7.0.1:
|
||||
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
dev: true
|
||||
|
||||
/finalhandler/1.2.0:
|
||||
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
statuses: 2.0.1
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/follow-redirects/1.15.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/forwarded/0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/fresh/0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/function-bind/1.1.1:
|
||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
||||
dev: true
|
||||
|
||||
/get-caller-file/2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
dev: false
|
||||
|
||||
/get-intrinsic/1.1.3:
|
||||
resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==}
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
has: 1.0.3
|
||||
has-symbols: 1.0.3
|
||||
dev: true
|
||||
|
||||
/has-flag/4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/has-symbols/1.0.3:
|
||||
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/has/1.0.3:
|
||||
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
dev: true
|
||||
|
||||
/http-errors/2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
dev: true
|
||||
|
||||
/http-proxy-middleware/2.0.6:
|
||||
resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@types/express': ^4.17.13
|
||||
peerDependenciesMeta:
|
||||
'@types/express':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/http-proxy': 1.17.9
|
||||
http-proxy: 1.18.1
|
||||
is-glob: 4.0.3
|
||||
is-plain-obj: 3.0.0
|
||||
micromatch: 4.0.5
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: true
|
||||
|
||||
/http-proxy/1.18.1:
|
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.15.2
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: true
|
||||
|
||||
/iconv-lite/0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
dev: true
|
||||
|
||||
/inherits/2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: true
|
||||
|
||||
/ipaddr.js/1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
dev: true
|
||||
|
||||
/is-extglob/2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-fullwidth-code-point/3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/is-glob/4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
dev: true
|
||||
|
||||
/is-number/7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: true
|
||||
|
||||
/is-plain-obj/3.0.0:
|
||||
resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/lodash/4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
dev: false
|
||||
|
||||
/media-typer/0.3.0:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/merge-descriptors/1.0.1:
|
||||
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||
dev: true
|
||||
|
||||
/methods/1.1.2:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/micromatch/4.0.5:
|
||||
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
|
||||
engines: {node: '>=8.6'}
|
||||
dependencies:
|
||||
braces: 3.0.2
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/mime-db/1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types/2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/mime/1.6.0:
|
||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ms/2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: true
|
||||
|
||||
/ms/2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/negotiator/0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/object-inspect/1.12.2:
|
||||
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||
dev: true
|
||||
|
||||
/on-finished/2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
dev: true
|
||||
|
||||
/parseurl/1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/path-to-regexp/0.1.7:
|
||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||
dev: true
|
||||
|
||||
/picomatch/2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
dev: true
|
||||
|
||||
/proxy-addr/2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
dev: true
|
||||
|
||||
/qs/6.11.0:
|
||||
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||
engines: {node: '>=0.6'}
|
||||
dependencies:
|
||||
side-channel: 1.0.4
|
||||
dev: true
|
||||
|
||||
/range-parser/1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/raw-body/2.5.1:
|
||||
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
dev: true
|
||||
|
||||
/require-directory/2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/requires-port/1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: true
|
||||
|
||||
/rxjs/7.5.7:
|
||||
resolution: {integrity: sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==}
|
||||
dependencies:
|
||||
tslib: 2.4.1
|
||||
dev: false
|
||||
|
||||
/safe-buffer/5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: true
|
||||
|
||||
/safer-buffer/2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: true
|
||||
|
||||
/send/0.18.0:
|
||||
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.0
|
||||
mime: 1.6.0
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/serve-static/1.15.0:
|
||||
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.18.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/setprototypeof/1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
dev: true
|
||||
|
||||
/shell-quote/1.7.4:
|
||||
resolution: {integrity: sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==}
|
||||
dev: false
|
||||
|
||||
/side-channel/1.0.4:
|
||||
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
get-intrinsic: 1.1.3
|
||||
object-inspect: 1.12.2
|
||||
dev: true
|
||||
|
||||
/spawn-command/0.0.2-1:
|
||||
resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==}
|
||||
dev: false
|
||||
|
||||
/statuses/2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/string-width/4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
dev: false
|
||||
|
||||
/strip-ansi/6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
dev: false
|
||||
|
||||
/supports-color/7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
dev: false
|
||||
|
||||
/supports-color/8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
dev: false
|
||||
|
||||
/to-regex-range/5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
dev: true
|
||||
|
||||
/toidentifier/1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
dev: true
|
||||
|
||||
/tree-kill/1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/tslib/2.4.1:
|
||||
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||
dev: false
|
||||
|
||||
/type-is/1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
media-typer: 0.3.0
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/unpipe/1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/utils-merge/1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
dev: true
|
||||
|
||||
/vary/1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/wrap-ansi/7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
dev: false
|
||||
|
||||
/y18n/5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/yargs-parser/21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/yargs/17.6.2:
|
||||
resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.1.1
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
dev: false
|
@ -3,8 +3,6 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
const app = express();
|
||||
|
||||
const PORT = 5000;
|
||||
|
||||
app.use(
|
||||
"/api/",
|
||||
createProxyMiddleware({
|
||||
@ -16,10 +14,10 @@ app.use(
|
||||
app.use(
|
||||
"/",
|
||||
createProxyMiddleware({
|
||||
target: "http://localhost:5173",
|
||||
target: "http://localhost:3000",
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.listen(PORT);
|
||||
console.log(`Reverse proxy listening at http://localhost:${PORT}`);
|
||||
app.listen(5000);
|
||||
console.log("Reverse proxy listening at http://localhost:5000");
|
@ -8,4 +8,4 @@ GET_LIMIT_WINDOW_SECONDS=0.1
|
||||
LOG_LEVEL=warn
|
||||
|
||||
# Make cleanup interval very long to avoid automatic cleanup during tests
|
||||
CLEANUP_INTERVAL_SECONDS=99999
|
||||
CLEANUP_INTERVAL_SECONDS=99999
|
||||
|
@ -11,8 +11,7 @@
|
||||
"test:coverage": "dotenv -e .env.test -- vitest run --no-threads --coverage",
|
||||
"test:db:reset": "dotenv -e .env.test -- npx prisma migrate reset -f",
|
||||
"build": "npx tsc",
|
||||
"dev": "npx nodemon src/server.ts | npx pino-colada",
|
||||
"migrate": "npx prisma migrate dev"
|
||||
"dev": "npx nodemon src/server.ts | npx pino-colada"
|
||||
},
|
||||
"author": "Maxime Cannoodt (mcndt)",
|
||||
"license": "MIT",
|
||||
|
@ -0,0 +1,10 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "EncryptedEmbed" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"note_id" TEXT NOT NULL,
|
||||
"embed_id" TEXT NOT NULL,
|
||||
"ciphertext" BLOB NOT NULL,
|
||||
"hmac" TEXT NOT NULL,
|
||||
"size_bytes" INTEGER NOT NULL,
|
||||
CONSTRAINT "EncryptedEmbed_note_id_fkey" FOREIGN KEY ("note_id") REFERENCES "EncryptedNote" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[note_id,embed_id]` on the table `EncryptedEmbed` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "EncryptedEmbed_note_id_embed_id_key" ON "EncryptedEmbed"("note_id", "embed_id");
|
@ -0,0 +1,17 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_EncryptedEmbed" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"note_id" TEXT NOT NULL,
|
||||
"embed_id" TEXT NOT NULL,
|
||||
"ciphertext" BLOB NOT NULL,
|
||||
"hmac" TEXT NOT NULL,
|
||||
"size_bytes" INTEGER NOT NULL,
|
||||
CONSTRAINT "EncryptedEmbed_note_id_fkey" FOREIGN KEY ("note_id") REFERENCES "EncryptedNote" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_EncryptedEmbed" ("ciphertext", "embed_id", "hmac", "id", "note_id", "size_bytes") SELECT "ciphertext", "embed_id", "hmac", "id", "note_id", "size_bytes" FROM "EncryptedEmbed";
|
||||
DROP TABLE "EncryptedEmbed";
|
||||
ALTER TABLE "new_EncryptedEmbed" RENAME TO "EncryptedEmbed";
|
||||
CREATE UNIQUE INDEX "EncryptedEmbed_note_id_embed_id_key" ON "EncryptedEmbed"("note_id", "embed_id");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
@ -1,16 +0,0 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_EncryptedNote" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"insert_time" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expire_time" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ciphertext" TEXT NOT NULL,
|
||||
"hmac" TEXT,
|
||||
"iv" TEXT,
|
||||
"crypto_version" TEXT NOT NULL DEFAULT 'v1'
|
||||
);
|
||||
INSERT INTO "new_EncryptedNote" ("ciphertext", "crypto_version", "expire_time", "hmac", "id", "insert_time") SELECT "ciphertext", "crypto_version", "expire_time", "hmac", "id", "insert_time" FROM "EncryptedNote";
|
||||
DROP TABLE "EncryptedNote";
|
||||
ALTER TABLE "new_EncryptedNote" RENAME TO "EncryptedNote";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EncryptedNote" ADD COLUMN "secret_token" TEXT;
|
@ -2,7 +2,8 @@
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["interactiveTransactions"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@ -11,14 +12,25 @@ datasource db {
|
||||
}
|
||||
|
||||
model EncryptedNote {
|
||||
id String @id @default(cuid())
|
||||
insert_time DateTime @default(now())
|
||||
expire_time DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
insert_time DateTime @default(now())
|
||||
expire_time DateTime @default(now())
|
||||
ciphertext String
|
||||
hmac String?
|
||||
iv String?
|
||||
crypto_version String @default("v1")
|
||||
secret_token String?
|
||||
hmac String
|
||||
crypto_version String @default("v1")
|
||||
EncryptedEmbed EncryptedEmbed[]
|
||||
}
|
||||
|
||||
model EncryptedEmbed {
|
||||
id String @id @default(cuid())
|
||||
note_id String
|
||||
embed_id String
|
||||
ciphertext Bytes
|
||||
hmac String
|
||||
size_bytes Int
|
||||
note EncryptedNote @relation(fields: [note_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([note_id, embed_id], name: "noteId_embedId")
|
||||
}
|
||||
|
||||
model event {
|
||||
|
@ -4,6 +4,8 @@ import { describe, it, expect } from "vitest";
|
||||
import prisma from "./db/client";
|
||||
import { deleteExpiredNotes } from "./tasks/deleteExpiredNotes";
|
||||
import { EventType } from "./logging/EventLogger";
|
||||
import { createNote } from "./db/note.dao";
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
|
||||
// const testNote with base64 ciphertext and hmac
|
||||
const testNote = {
|
||||
@ -22,7 +24,7 @@ describe("GET /api/note", () => {
|
||||
const res = await supertest(app).get(`/api/note/${id}`);
|
||||
|
||||
// Validate returned note
|
||||
expect(res.statusCode).toBe(200);
|
||||
expectCodeOrThrowResponse(res, 200);
|
||||
expect(res.body).toHaveProperty("id");
|
||||
expect(res.body).toHaveProperty("expire_time");
|
||||
expect(res.body).toHaveProperty("insert_time");
|
||||
@ -48,7 +50,7 @@ describe("GET /api/note", () => {
|
||||
const res = await supertest(app).get(`/api/note/NaN`);
|
||||
|
||||
// Validate returned note
|
||||
expect(res.statusCode).toBe(404);
|
||||
expectCodeOrThrowResponse(res, 404);
|
||||
|
||||
// Is a read event logged?
|
||||
const readEvents = await prisma.event.findMany({
|
||||
@ -73,21 +75,40 @@ describe("GET /api/note", () => {
|
||||
const responseCodes = responses.map((res) => res.statusCode);
|
||||
|
||||
// at least one response should be 429
|
||||
expect(responseCodes).toContain(200);
|
||||
expect(responseCodes).toContain(429);
|
||||
|
||||
// No other response codes should be present
|
||||
expect(
|
||||
responseCodes.map((code) => code === 429 || code === 200)
|
||||
).not.toContain(false);
|
||||
|
||||
// sleep for 100 ms to allow rate limiter to reset
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/note", () => {
|
||||
it("returns a view_url on correct POST body (without plugin version and user id)", async () => {
|
||||
const res = await supertest(app).post("/api/note").send(testNote);
|
||||
it("returns a view_url on correct POST body with embeds", async () => {
|
||||
const res = await supertest(app)
|
||||
.post("/api/note")
|
||||
.send({
|
||||
...testNote,
|
||||
embeds: [
|
||||
{
|
||||
embed_id: "sample_embed_id0",
|
||||
ciphertext: Buffer.from("sample_ciphertext").toString("base64"),
|
||||
hmac: Buffer.from("sample_hmac").toString("base64"),
|
||||
},
|
||||
{
|
||||
embed_id: "sample_embed_id1",
|
||||
ciphertext: Buffer.from("sample_ciphertext").toString("base64"),
|
||||
hmac: Buffer.from("sample_hmac").toString("base64"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
console.log(res.body);
|
||||
}
|
||||
expect(res.statusCode).toBe(200);
|
||||
expectCodeOrThrowResponse(res, 200);
|
||||
|
||||
// Returned body has correct fields
|
||||
expect(res.body).toHaveProperty("expire_time");
|
||||
@ -115,7 +136,7 @@ describe("POST /api/note", () => {
|
||||
|
||||
it("Returns a bad request on invalid POST body", async () => {
|
||||
const res = await supertest(app).post("/api/note").send({});
|
||||
expect(res.statusCode).toBe(400);
|
||||
expectCodeOrThrowResponse(res, 400);
|
||||
});
|
||||
|
||||
it("returns a valid view_url on correct POST body", async () => {
|
||||
@ -123,7 +144,7 @@ describe("POST /api/note", () => {
|
||||
let res = await supertest(app).post("/api/note").send(testNote);
|
||||
|
||||
// Extract note id from post response
|
||||
expect(res.statusCode).toBe(200);
|
||||
expectCodeOrThrowResponse(res, 200);
|
||||
expect(res.body).toHaveProperty("view_url");
|
||||
const match = (res.body.view_url as string).match(/note\/(.+)$/);
|
||||
expect(match).not.toBeNull();
|
||||
@ -134,7 +155,7 @@ describe("POST /api/note", () => {
|
||||
res = await supertest(app).get(`/api/note/${note_id}`);
|
||||
|
||||
// Validate returned note
|
||||
expect(res.statusCode).toBe(200);
|
||||
expectCodeOrThrowResponse(res, 200);
|
||||
expect(res.body).toHaveProperty("id");
|
||||
expect(res.body).toHaveProperty("expire_time");
|
||||
expect(res.body).toHaveProperty("insert_time");
|
||||
@ -145,16 +166,17 @@ describe("POST /api/note", () => {
|
||||
expect(res.body.hmac).toEqual(testNote.hmac);
|
||||
});
|
||||
|
||||
it("Applies upload limit to endpoint of 500kb", async () => {
|
||||
it("Applies upload limit to endpoint of 8MB", async () => {
|
||||
const largeNote = {
|
||||
ciphertext: "a".repeat(500 * 1024),
|
||||
hmac: "sample_hmac",
|
||||
ciphertext: Buffer.from("a".repeat(8 * 1024 * 1024)).toString("base64"),
|
||||
hmac: Buffer.from("a".repeat(32)).toString("base64"),
|
||||
};
|
||||
const res = await supertest(app).post("/api/note").send(largeNote);
|
||||
expect(res.statusCode).toBe(413);
|
||||
expectCodeOrThrowResponse(res, 413);
|
||||
});
|
||||
|
||||
it("Applies rate limits to endpoint", async () => {
|
||||
// 2022-08-30: Skip this test because it crashes the database connection for some reason
|
||||
it.skip("Applies rate limits to endpoint", async () => {
|
||||
// make more requests than the post limit set in .env.test
|
||||
const requests = [];
|
||||
for (let i = 0; i < 51; i++) {
|
||||
@ -172,11 +194,52 @@ describe("POST /api/note", () => {
|
||||
responseCodes.map((code) => code === 429 || code === 200)
|
||||
).not.toContain(false);
|
||||
|
||||
// sleep for 100 ms to allow rate limiter to reset
|
||||
// sleep for 250 ms to allow rate limiter to reset
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Use case: POST note with embeds, then GET embeds", () => {
|
||||
it("returns a view_url on correct POST body with embeds", async () => {
|
||||
const payload = {
|
||||
ciphertext: Buffer.from("sample_ciphertext").toString("base64"),
|
||||
hmac: Buffer.from("sample_hmac").toString("base64"),
|
||||
user_id: "f06536e7df6857fc",
|
||||
embeds: [
|
||||
{
|
||||
embed_id: "EMBED_ID",
|
||||
ciphertext: Buffer.from("sample_ciphertext").toString("base64"),
|
||||
hmac: Buffer.from("sample_hmac").toString("base64"),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// make post request
|
||||
const res = await supertest(app).post("/api/note").send(payload);
|
||||
|
||||
// check response and extract note id
|
||||
expectCodeOrThrowResponse(res, 200);
|
||||
expect(res.body).toHaveProperty("view_url");
|
||||
const match = (res.body.view_url as string).match(/note\/(.+)$/);
|
||||
expect(match).not.toBeNull();
|
||||
const note_id = (match as RegExpMatchArray)[1];
|
||||
|
||||
// make get request for note
|
||||
const noteRes = await supertest(app).get(`/api/note/${note_id}`);
|
||||
expectCodeOrThrowResponse(noteRes, 200);
|
||||
expect(noteRes.body?.ciphertext).toEqual(payload.ciphertext);
|
||||
expect(noteRes.body?.hmac).toEqual(payload.hmac);
|
||||
|
||||
// make get request for embed
|
||||
const embedRes = await supertest(app).get(
|
||||
`/api/note/${note_id}/embeds/EMBED_ID`
|
||||
);
|
||||
expectCodeOrThrowResponse(embedRes, 200);
|
||||
expect(embedRes.body?.ciphertext).toEqual(payload.embeds[0].ciphertext);
|
||||
expect(embedRes.body?.hmac).toEqual(payload.embeds[0].hmac);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Clean expired notes", () => {
|
||||
it("removes expired notes", async () => {
|
||||
// insert a note with expiry date in the past using prisma
|
||||
@ -212,4 +275,54 @@ describe("Clean expired notes", () => {
|
||||
testNote.ciphertext.length + testNote.hmac.length
|
||||
);
|
||||
});
|
||||
|
||||
it("removes notes with embeds", async () => {
|
||||
// insert a note with embeds and with expiry date in the past using prisma
|
||||
const note = {
|
||||
...testNote,
|
||||
expire_time: new Date(0),
|
||||
} as EncryptedNote;
|
||||
const embeds = [
|
||||
{
|
||||
embed_id: "EMBED_ID",
|
||||
ciphertext: Buffer.from("sample_ciphertext").toString("base64"),
|
||||
hmac: Buffer.from("sample_hmac").toString("base64"),
|
||||
},
|
||||
];
|
||||
const { id } = await createNote(note, embeds);
|
||||
|
||||
// make request for note and check that response is 200
|
||||
const res = await supertest(app).get(`/api/note/${id}`);
|
||||
expect(res.statusCode).toBe(200);
|
||||
const embedRes = await supertest(app).get(
|
||||
`/api/note/${id}/embeds/EMBED_ID`
|
||||
);
|
||||
expect(embedRes.statusCode).toBe(200);
|
||||
|
||||
// run cleanup
|
||||
const nDeleted = await deleteExpiredNotes();
|
||||
expect(nDeleted).toBeGreaterThan(0);
|
||||
|
||||
// if the note is added to the expire filter, it returns 410
|
||||
const res2 = await supertest(app).get(`/api/note/${id}`);
|
||||
expect(res2.statusCode).toBe(410);
|
||||
|
||||
// check that the embed is not found
|
||||
const embedRes2 = await supertest(app).get(
|
||||
`/api/note/${id}/embeds/EMBED_ID`
|
||||
);
|
||||
expect(embedRes2.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
function expectCodeOrThrowResponse(res: supertest.Response, expected: number) {
|
||||
try {
|
||||
expect(res.status).toBe(expected);
|
||||
} catch (e) {
|
||||
(e as Error).message = `
|
||||
Unexpected status ${res.status} (expected ${expected}):
|
||||
|
||||
Response body: ${res.text}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,6 @@ import { deleteExpiredNotes, deleteInterval } from "./tasks/deleteExpiredNotes";
|
||||
// Initialize middleware clients
|
||||
export const app: Express = express();
|
||||
|
||||
// Enable JSON body parsing
|
||||
app.use(express.json({}));
|
||||
|
||||
// configure logging
|
||||
app.use(
|
||||
pinoHttp({
|
||||
|
20
server/src/controllers/note/embeds/embeds.get.controller.ts
Normal file
20
server/src/controllers/note/embeds/embeds.get.controller.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { getEmbed } from "../../../db/embed.dao";
|
||||
|
||||
export async function getEmbedController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
const { id: note_id, embed_id } = req.params;
|
||||
try {
|
||||
const embed = await getEmbed(note_id, embed_id);
|
||||
if (embed != null) {
|
||||
res.status(200).json(embed).send();
|
||||
} else {
|
||||
res.status(404).send();
|
||||
}
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import express from "express";
|
||||
import supertest from "supertest";
|
||||
import { vi, it, expect, describe, beforeEach, afterEach } from "vitest";
|
||||
import { getEmbedController } from "./embeds.get.controller";
|
||||
import * as embedDao from "../../../db/embed.dao";
|
||||
|
||||
vi.mock("../../../db/embed.dao");
|
||||
|
||||
const MOCK_EMBED_DTO: embedDao.EncryptedEmbedDTO = {
|
||||
note_id: "valid_note_id",
|
||||
embed_id: "valid_embed_id",
|
||||
ciphertext: Buffer.from("sample_ciphertext").toString("base64"),
|
||||
hmac: Buffer.from("sample_hmac").toString("base64"),
|
||||
};
|
||||
|
||||
describe("Test GET embeds", () => {
|
||||
let app: express.Express;
|
||||
let mockEmbedDao = vi.mocked(embedDao);
|
||||
|
||||
beforeEach(() => {
|
||||
app = express()
|
||||
.use(express.json())
|
||||
.get("/:id/embeds/:embed_id", getEmbedController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("Should return 200 for a valid note_id+embed_id pair", async () => {
|
||||
// mock db response
|
||||
mockEmbedDao.getEmbed.mockImplementation(async (noteId, embedId) => {
|
||||
if (
|
||||
noteId === MOCK_EMBED_DTO.note_id &&
|
||||
embedId === MOCK_EMBED_DTO.embed_id
|
||||
) {
|
||||
return MOCK_EMBED_DTO;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// make request
|
||||
const res = await supertest(app).get(
|
||||
"/valid_note_id/embeds/valid_embed_id"
|
||||
);
|
||||
|
||||
// check response
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject(MOCK_EMBED_DTO);
|
||||
});
|
||||
|
||||
it("Should return 404 for an invalid note_id+embed_id pair", async () => {
|
||||
// mock db response
|
||||
mockEmbedDao.getEmbed.mockImplementation(async (noteId, embedId) => {
|
||||
if (
|
||||
noteId === MOCK_EMBED_DTO.note_id &&
|
||||
embedId === MOCK_EMBED_DTO.embed_id
|
||||
) {
|
||||
return MOCK_EMBED_DTO;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// make request
|
||||
const res = await supertest(app).get(
|
||||
"/invalid_note_id/embeds/invalid_embed_id"
|
||||
);
|
||||
|
||||
// check response
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it("Should return 500 on database failure", async () => {
|
||||
// mock db response
|
||||
mockEmbedDao.getEmbed.mockImplementation(async (noteId, embedId) => {
|
||||
throw new Error("Database failure");
|
||||
});
|
||||
|
||||
// make request
|
||||
const res = await supertest(app).get(
|
||||
"/valid_note_id/embeds/valid_embed_id"
|
||||
);
|
||||
|
||||
// check response
|
||||
expect(res.statusCode).toBe(500);
|
||||
});
|
||||
});
|
6
server/src/controllers/note/embeds/embeds.router.ts
Normal file
6
server/src/controllers/note/embeds/embeds.router.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import express from "express";
|
||||
import { getEmbedController } from "./embeds.get.controller";
|
||||
|
||||
export const embedsRoute = express.Router({ mergeParams: true });
|
||||
|
||||
embedsRoute.get("/:embed_id", getEmbedController);
|
@ -1,77 +0,0 @@
|
||||
import { validateOrReject, ValidationError } from "class-validator";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { deleteNote, getNote } from "../../db/note.dao";
|
||||
import checkId from "../../lib/checkUserId";
|
||||
import { getNoteFilter } from "../../lib/expiredNoteFilter";
|
||||
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
|
||||
import { getConnectingIp, getNoteSize } from "../../util";
|
||||
import { NoteDeleteRequest } from "../../validation/Request";
|
||||
|
||||
export async function deleteNoteController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
const event: WriteEvent = {
|
||||
success: false,
|
||||
host: getConnectingIp(req),
|
||||
user_id: req.body.user_id,
|
||||
user_plugin_version: req.body.plugin_version,
|
||||
};
|
||||
|
||||
// Validate request body
|
||||
const noteDeleteRequest = new NoteDeleteRequest();
|
||||
Object.assign(noteDeleteRequest, req.body);
|
||||
try {
|
||||
await validateOrReject(noteDeleteRequest);
|
||||
} catch (_err: any) {
|
||||
const err = _err as ValidationError;
|
||||
res.status(400).send(err.toString());
|
||||
event.error = err.toString();
|
||||
await EventLogger.deleteEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate user ID, if present
|
||||
if (noteDeleteRequest.user_id && !checkId(noteDeleteRequest.user_id)) {
|
||||
console.log("invalid user id");
|
||||
res.status(400).send("Invalid user id (checksum failed)");
|
||||
event.error = "Invalid user id (checksum failed)";
|
||||
EventLogger.deleteEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// get note from db
|
||||
const note = await getNote(req.params.id);
|
||||
if (!note) {
|
||||
res.status(404).send("Note not found");
|
||||
event.error = "Note not found";
|
||||
await EventLogger.deleteEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate secret token
|
||||
if (note.secret_token !== req.body.secret_token) {
|
||||
res.status(401).send("Invalid token");
|
||||
event.error = "Invalid secret token";
|
||||
await EventLogger.deleteEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete note
|
||||
try {
|
||||
await deleteNote(note.id);
|
||||
res.status(200).send();
|
||||
event.success = true;
|
||||
event.note_id = note.id;
|
||||
event.size_bytes = getNoteSize(note);
|
||||
await EventLogger.deleteEvent(event);
|
||||
} catch (err) {
|
||||
event.error = (err as Error).toString();
|
||||
await EventLogger.deleteEvent(event);
|
||||
next(err);
|
||||
}
|
||||
|
||||
const filter = await getNoteFilter("deletedNotes");
|
||||
await filter.addNoteIds([note.id]);
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
import express from "express";
|
||||
import supertest from "supertest";
|
||||
import { vi, describe, it, beforeEach, afterEach, expect } from "vitest";
|
||||
import * as noteDao from "../../db/note.dao";
|
||||
import * as bloomFilter from "../../db/bloomFilter.dao";
|
||||
import EventLogger from "../../logging/EventLogger";
|
||||
import { deleteNoteController } from "./note.delete.controller";
|
||||
|
||||
vi.mock("../../db/note.dao");
|
||||
vi.mock("../../db/bloomFilter.dao");
|
||||
vi.mock("../../logging/EventLogger");
|
||||
|
||||
const VALID_USER_ID = "f06536e7df6857fc";
|
||||
|
||||
const MOCK_SECRET_TOKEN = "U0VDUkVUX1RPS0VO";
|
||||
const MOCK_NOTE_ID = "NOTE_ID";
|
||||
|
||||
describe("note.delete.controller", () => {
|
||||
let mockNoteDao = vi.mocked(noteDao);
|
||||
let mockEventLogger = vi.mocked(EventLogger);
|
||||
let mockBloomFilterDao = vi.mocked(bloomFilter);
|
||||
|
||||
const test_app = express()
|
||||
.use(express.json())
|
||||
.delete("/:id", deleteNoteController);
|
||||
|
||||
beforeEach(() => {
|
||||
mockNoteDao.getNote.mockImplementation(async (noteId) => {
|
||||
if (noteId === MOCK_NOTE_ID) {
|
||||
return {
|
||||
id: MOCK_NOTE_ID,
|
||||
secret_token: MOCK_SECRET_TOKEN,
|
||||
} as EncryptedNote;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
mockNoteDao.deleteNote.mockImplementation(async (id) => {
|
||||
if (id === MOCK_NOTE_ID) {
|
||||
return vi.fn() as unknown as EncryptedNote;
|
||||
} else {
|
||||
throw new Error("Note not found");
|
||||
}
|
||||
});
|
||||
|
||||
mockBloomFilterDao.getFilter.mockImplementation(async () => {
|
||||
throw new Error("No BloomFilter found");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("Should delete a note with a valid secret token", async () => {
|
||||
const response = await supertest(test_app)
|
||||
.delete(`/${MOCK_NOTE_ID}`)
|
||||
.send({ user_id: VALID_USER_ID, secret_token: MOCK_SECRET_TOKEN });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockNoteDao.deleteNote).toBeCalledWith(MOCK_NOTE_ID);
|
||||
expect(mockEventLogger.deleteEvent).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
note_id: MOCK_NOTE_ID,
|
||||
user_id: VALID_USER_ID,
|
||||
success: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("Should return 401 for an invalid secret token", async () => {
|
||||
const response = await supertest(test_app)
|
||||
.delete(`/${MOCK_NOTE_ID}`)
|
||||
.send({ user_id: VALID_USER_ID, secret_token: "0000" });
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(mockNoteDao.deleteNote).not.toBeCalled();
|
||||
expect(mockEventLogger.deleteEvent).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
user_id: VALID_USER_ID,
|
||||
success: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("Should return 404 for a note that does not exist", async () => {
|
||||
const response = await supertest(test_app)
|
||||
.delete("/0000")
|
||||
.send({ user_id: VALID_USER_ID, secret_token: MOCK_SECRET_TOKEN });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(mockNoteDao.deleteNote).not.toBeCalled();
|
||||
expect(mockEventLogger.deleteEvent).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
user_id: VALID_USER_ID,
|
||||
success: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
@ -1,8 +1,9 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { getNoteFilter } from "../../lib/expiredNoteFilter";
|
||||
import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter";
|
||||
import EventLogger from "../../logging/EventLogger";
|
||||
import { getConnectingIp, getNoteSize } from "../../util";
|
||||
import { getConnectingIp } from "../../util";
|
||||
import { getNote } from "../../db/note.dao";
|
||||
|
||||
export async function getNoteController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@ -16,23 +17,13 @@ export async function getNoteController(
|
||||
success: true,
|
||||
host: ip,
|
||||
note_id: note.id,
|
||||
size_bytes: getNoteSize(note),
|
||||
size_bytes: note.ciphertext.length + note.hmac.length,
|
||||
});
|
||||
res.send(note);
|
||||
} else {
|
||||
// check the expired filter to see if the note was expired
|
||||
const deletedFilter = await getNoteFilter("deletedNotes");
|
||||
const expiredFilter = await getNoteFilter("expiredNotes");
|
||||
|
||||
if (deletedFilter.hasNoteId(req.params.id)) {
|
||||
await EventLogger.readEvent({
|
||||
success: false,
|
||||
host: ip,
|
||||
note_id: req.params.id,
|
||||
error: "Note deleted",
|
||||
});
|
||||
res.status(410).send("Note deleted");
|
||||
} else if (expiredFilter.hasNoteId(req.params.id)) {
|
||||
const expiredFilter = await getExpiredNoteFilter();
|
||||
if (expiredFilter.hasNoteId(req.params.id)) {
|
||||
await EventLogger.readEvent({
|
||||
success: false,
|
||||
host: ip,
|
||||
@ -47,7 +38,7 @@ export async function getNoteController(
|
||||
note_id: req.params.id,
|
||||
error: "Note not found",
|
||||
});
|
||||
res.status(404).send("Note not found");
|
||||
res.status(404).send();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,16 +1,64 @@
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
import { EncryptedNote, PrismaClient } from "@prisma/client";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { crc16 as crc } from "crc";
|
||||
import { createNote } from "../../db/note.dao";
|
||||
import { addDays, getConnectingIp, getNoteSize } from "../../util";
|
||||
import { addDays, getConnectingIp } from "../../util";
|
||||
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
|
||||
import { validateOrReject, ValidationError } from "class-validator";
|
||||
import { generateToken } from "../../crypto/GenerateToken";
|
||||
import { NotePostRequest } from "../../validation/Request";
|
||||
import checkId from "../../lib/checkUserId";
|
||||
import {
|
||||
validateOrReject,
|
||||
IsBase64,
|
||||
IsHexadecimal,
|
||||
IsNotEmpty,
|
||||
ValidateIf,
|
||||
ValidationError,
|
||||
Matches,
|
||||
IsString,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import prisma from "../../db/client";
|
||||
|
||||
export class EncryptedEmbedBody {
|
||||
@IsBase64()
|
||||
@IsNotEmpty()
|
||||
ciphertext!: string;
|
||||
|
||||
@IsBase64()
|
||||
@IsNotEmpty()
|
||||
hmac!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
embed_id!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request body for creating a note
|
||||
*/
|
||||
export class NotePostRequest {
|
||||
@IsBase64()
|
||||
@IsNotEmpty()
|
||||
ciphertext: string | undefined;
|
||||
|
||||
@IsBase64()
|
||||
@IsNotEmpty()
|
||||
hmac: string | undefined;
|
||||
|
||||
@ValidateIf((o) => o.user_id != null)
|
||||
@IsHexadecimal()
|
||||
user_id: string | undefined;
|
||||
|
||||
@ValidateIf((o) => o.plugin_version != null)
|
||||
@Matches("^[0-9]+\\.[0-9]+\\.[0-9]+$")
|
||||
plugin_version: string | undefined;
|
||||
|
||||
@Matches("^v[0-9]+$")
|
||||
crypto_version: string = "v1";
|
||||
|
||||
// validate the shape of each item manually, avoid need for class-transformer package
|
||||
@IsArray()
|
||||
embeds: EncryptedEmbedBody[] = [];
|
||||
}
|
||||
|
||||
export async function postNoteController(
|
||||
req: Request,
|
||||
@ -26,9 +74,18 @@ export async function postNoteController(
|
||||
|
||||
// Validate request body
|
||||
const notePostRequest = new NotePostRequest();
|
||||
const noteEmbedRequests: EncryptedEmbedBody[] = [];
|
||||
Object.assign(notePostRequest, req.body);
|
||||
try {
|
||||
await validateOrReject(notePostRequest);
|
||||
if (notePostRequest.embeds && notePostRequest.embeds.length > 0) {
|
||||
for (const embed of notePostRequest.embeds) {
|
||||
const embedBody = new EncryptedEmbedBody();
|
||||
Object.assign(embedBody, embed);
|
||||
await validateOrReject(embedBody);
|
||||
noteEmbedRequests.push(embedBody);
|
||||
}
|
||||
}
|
||||
} catch (_err: any) {
|
||||
const err = _err as ValidationError;
|
||||
res.status(400).send(err.toString());
|
||||
@ -39,7 +96,6 @@ export async function postNoteController(
|
||||
|
||||
// Validate user ID, if present
|
||||
if (notePostRequest.user_id && !checkId(notePostRequest.user_id)) {
|
||||
console.log("invalid user id");
|
||||
res.status(400).send("Invalid user id (checksum failed)");
|
||||
event.error = "Invalid user id (checksum failed)";
|
||||
EventLogger.writeEvent(event);
|
||||
@ -48,35 +104,57 @@ export async function postNoteController(
|
||||
|
||||
// Create note object
|
||||
const EXPIRE_WINDOW_DAYS = 30;
|
||||
const secret_token = generateToken();
|
||||
|
||||
const note = {
|
||||
ciphertext: notePostRequest.ciphertext as string,
|
||||
hmac: notePostRequest.hmac as string,
|
||||
iv: notePostRequest.iv as string,
|
||||
expire_time: addDays(new Date(), EXPIRE_WINDOW_DAYS),
|
||||
crypto_version: notePostRequest.crypto_version,
|
||||
secret_token: secret_token,
|
||||
} as EncryptedNote;
|
||||
|
||||
// Store note object
|
||||
createNote(note)
|
||||
.then(async (savedNote) => {
|
||||
event.success = true;
|
||||
event.note_id = savedNote.id;
|
||||
event.size_bytes = getNoteSize(note);
|
||||
event.expire_window_days = EXPIRE_WINDOW_DAYS;
|
||||
await EventLogger.writeEvent(event);
|
||||
res.json({
|
||||
view_url: `${process.env.FRONTEND_URL}/note/${savedNote.id}`,
|
||||
expire_time: savedNote.expire_time,
|
||||
secret_token: savedNote.secret_token,
|
||||
note_id: savedNote.id,
|
||||
});
|
||||
})
|
||||
.catch(async (err) => {
|
||||
event.error = err.toString();
|
||||
await EventLogger.writeEvent(event);
|
||||
next(err);
|
||||
// Store note object and possible embeds in database transaction
|
||||
try {
|
||||
const savedNote = await createNote(note, noteEmbedRequests);
|
||||
|
||||
// Log write event
|
||||
event.success = true;
|
||||
event.note_id = savedNote.id;
|
||||
event.size_bytes = savedNote.ciphertext.length + savedNote.hmac.length;
|
||||
event.expire_window_days = EXPIRE_WINDOW_DAYS;
|
||||
await EventLogger.writeEvent(event);
|
||||
|
||||
// return HTTP request
|
||||
res.json({
|
||||
view_url: `${process.env.FRONTEND_URL}/note/${savedNote.id}`,
|
||||
expire_time: savedNote.expire_time,
|
||||
});
|
||||
} catch (err: any) {
|
||||
// if the error matches "Duplicate embed", return a 409 conflict
|
||||
event.error = err.toString();
|
||||
await EventLogger.writeEvent(event);
|
||||
if (err.message.includes("Duplicate embed")) {
|
||||
res.status(409).send(err.message);
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id {string} a 16 character base16 string with 12 random characters and 4 CRC characters
|
||||
* @returns {boolean} true if the id is valid, false otherwise
|
||||
*/
|
||||
function checkId(id: string): boolean {
|
||||
// check length
|
||||
if (id.length !== 16) {
|
||||
return false;
|
||||
}
|
||||
// extract the random number and the checksum
|
||||
const random = id.slice(0, 12);
|
||||
const checksum = id.slice(12, 16);
|
||||
|
||||
// compute the CRC of the random number
|
||||
const computedChecksum = crc(random).toString(16).padStart(4, "0");
|
||||
|
||||
// compare the computed checksum with the one in the id
|
||||
return computedChecksum === checksum;
|
||||
}
|
||||
|
@ -2,11 +2,16 @@ import express from "express";
|
||||
import supertest from "supertest";
|
||||
import { vi, describe, it, beforeEach, afterEach, expect } from "vitest";
|
||||
import * as noteDao from "../../db/note.dao";
|
||||
import * as embedDao from "../../db/embed.dao";
|
||||
import EventLogger from "../../logging/EventLogger";
|
||||
import { NotePostRequest } from "../../validation/Request";
|
||||
import { postNoteController } from "./note.post.controller";
|
||||
import {
|
||||
EncryptedEmbedBody,
|
||||
NotePostRequest,
|
||||
postNoteController,
|
||||
} from "./note.post.controller";
|
||||
|
||||
vi.mock("../../db/note.dao");
|
||||
vi.mock("../../db/embed.dao");
|
||||
vi.mock("../../logging/EventLogger");
|
||||
|
||||
const VALID_CIPHERTEXT = Buffer.from("sample_ciphertext").toString("base64");
|
||||
@ -20,8 +25,10 @@ const VALID_CRYPTO_VERSION = "v99";
|
||||
const MALFORMED_CRYPTO_VERSION = "32";
|
||||
|
||||
const MOCK_NOTE_ID = "1234";
|
||||
const MOCK_EMBED_ID = "abcd";
|
||||
|
||||
type TestParams = {
|
||||
case: string;
|
||||
payload: Partial<NotePostRequest>;
|
||||
expectedStatus: number;
|
||||
};
|
||||
@ -29,6 +36,7 @@ type TestParams = {
|
||||
const TEST_PAYLOADS: TestParams[] = [
|
||||
// Request with valid ciphertext and hmac
|
||||
{
|
||||
case: "valid ciphertext and hmac",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -37,6 +45,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with valid ciphertext, hmac, user id, and plugin version
|
||||
{
|
||||
case: "valid ciphertext, hmac, user id, and plugin version",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -47,6 +56,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with non-base64 ciphertext
|
||||
{
|
||||
case: "non-base64 ciphertext",
|
||||
payload: {
|
||||
ciphertext: "not_base64",
|
||||
hmac: VALID_HMAC,
|
||||
@ -55,6 +65,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with non-base64 hmac
|
||||
{
|
||||
case: "non-base64 hmac",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: "not_base64",
|
||||
@ -63,6 +74,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with empty ciphertext
|
||||
{
|
||||
case: "empty ciphertext",
|
||||
payload: {
|
||||
ciphertext: "",
|
||||
hmac: VALID_HMAC,
|
||||
@ -71,6 +83,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with empty hmac
|
||||
{
|
||||
case: "empty hmac",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: "",
|
||||
@ -79,6 +92,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with valid user id
|
||||
{
|
||||
case: "valid user id",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -88,6 +102,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with malformed user id (wrong crc)
|
||||
{
|
||||
case: "malformed user id (wrong crc)",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -97,6 +112,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with malformed user id (wrong length)
|
||||
{
|
||||
case: "malformed user id (wrong length)",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -106,6 +122,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with valid plugin version
|
||||
{
|
||||
case: "valid plugin version",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -115,6 +132,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with malformed plugin version
|
||||
{
|
||||
case: "malformed plugin version",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -124,6 +142,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with valid ciphertext, hmac, user id, plugin version, and crypto version
|
||||
{
|
||||
case: "valid ciphertext, hmac, user id, plugin version, and crypto version",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -135,6 +154,7 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
// Request with malformed crypto version
|
||||
{
|
||||
case: "malformed crypto version",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
@ -144,33 +164,164 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
expectedStatus: 400,
|
||||
},
|
||||
// Request with empty embeds array
|
||||
{
|
||||
case: "empty embeds array",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
plugin_version: VALID_VERSION,
|
||||
crypto_version: VALID_CRYPTO_VERSION,
|
||||
embeds: [],
|
||||
},
|
||||
expectedStatus: 200,
|
||||
},
|
||||
// Request with single valid embed
|
||||
{
|
||||
case: "valid embeds array",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
embeds: [
|
||||
{
|
||||
embed_id: "0",
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedStatus: 200,
|
||||
},
|
||||
// Request with embed with empty embed_id
|
||||
{
|
||||
case: "embed with empty embed_id",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
embeds: [
|
||||
{
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
} as EncryptedEmbedBody,
|
||||
],
|
||||
},
|
||||
expectedStatus: 400,
|
||||
},
|
||||
// Request with embed with non-base64 ciphertext
|
||||
{
|
||||
case: "embed with non-base64 ciphertext",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
embeds: [
|
||||
{
|
||||
ciphertext: "not_base64",
|
||||
hmac: VALID_HMAC,
|
||||
embed_id: "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedStatus: 400,
|
||||
},
|
||||
// Request with embed with non-base64 hmac
|
||||
{
|
||||
case: "embed with non-base64 hmac",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
embeds: [
|
||||
{
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: "not_base64",
|
||||
embed_id: "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedStatus: 400,
|
||||
},
|
||||
// Request with duplicate embeds
|
||||
{
|
||||
case: "duplicate embeds",
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
embeds: [
|
||||
{
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
embed_id: "0",
|
||||
},
|
||||
{
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
embed_id: "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedStatus: 409,
|
||||
},
|
||||
];
|
||||
|
||||
describe("note.post.controller", () => {
|
||||
describe("Execute test cases", () => {
|
||||
let mockNoteDao = vi.mocked(noteDao);
|
||||
let mockEmbedDao = vi.mocked(embedDao);
|
||||
let mockEventLogger = vi.mocked(EventLogger);
|
||||
|
||||
const test_app = express().use(express.json()).post("/", postNoteController);
|
||||
|
||||
beforeEach(() => {
|
||||
// database writes always succeed
|
||||
mockNoteDao.createNote.mockImplementation(async (note) => ({
|
||||
...note,
|
||||
id: MOCK_NOTE_ID,
|
||||
insert_time: new Date(),
|
||||
}));
|
||||
const storedEmbeds: string[] = [];
|
||||
|
||||
mockNoteDao.createNote.mockImplementation(async (note, embeds) => {
|
||||
if (embeds && embeds.length > 0) {
|
||||
for (const e of embeds) {
|
||||
if (storedEmbeds.find((s) => s === MOCK_NOTE_ID + e.embed_id)) {
|
||||
throw new Error("Duplicate embed");
|
||||
}
|
||||
storedEmbeds.push(MOCK_NOTE_ID + e.embed_id);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...note,
|
||||
id: MOCK_NOTE_ID,
|
||||
insert_time: new Date(),
|
||||
};
|
||||
});
|
||||
mockEmbedDao.createEmbed.mockImplementation(async (embed) => {
|
||||
storedEmbeds.push(embed.note_id + embed.embed_id);
|
||||
return {
|
||||
...embed,
|
||||
ciphertext: Buffer.from(embed.ciphertext, "base64"),
|
||||
id: MOCK_EMBED_ID,
|
||||
size_bytes: embed.ciphertext.length,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it.each(TEST_PAYLOADS)("test payloads", async (params) => {
|
||||
it.each(TEST_PAYLOADS)("Case %#: $case", async (params) => {
|
||||
const { payload, expectedStatus } = params;
|
||||
|
||||
// make request
|
||||
const res = await supertest(test_app).post("/").send(payload);
|
||||
expect(res.status).toBe(expectedStatus);
|
||||
try {
|
||||
expect(res.status).toBe(expectedStatus);
|
||||
} catch (e) {
|
||||
throw new Error(`
|
||||
Unexpected status ${res.status} (expected ${expectedStatus}):
|
||||
|
||||
Response body: ${res.text}`);
|
||||
}
|
||||
|
||||
// Validate reponse body
|
||||
if (expectedStatus === 200) {
|
||||
@ -194,7 +345,8 @@ describe("note.post.controller", () => {
|
||||
hmac: payload.hmac,
|
||||
crypto_version: payload.crypto_version || "v1",
|
||||
expire_time: expect.any(Date),
|
||||
})
|
||||
}),
|
||||
expect.arrayContaining(payload?.embeds ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
import bodyParser from "body-parser";
|
||||
import express from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import { deleteNoteController } from "./note.delete.controller";
|
||||
import { embedsRoute } from "./embeds/embeds.router";
|
||||
import { getNoteController } from "./note.get.controller";
|
||||
import { postNoteController } from "./note.post.controller";
|
||||
|
||||
export const notesRoute = express.Router();
|
||||
|
||||
const jsonParser = express.json({ limit: "500k" });
|
||||
const jsonParser = express.json();
|
||||
const uploadLimit = bodyParser.json({ limit: "8mb" });
|
||||
|
||||
const postRateLimit = rateLimit({
|
||||
windowMs: parseFloat(process.env.POST_LIMIT_WINDOW_SECONDS as string) * 1000,
|
||||
@ -23,7 +25,8 @@ const getRateLimit = rateLimit({
|
||||
});
|
||||
|
||||
// notesRoute.use(jsonParser, uploadLimit);
|
||||
notesRoute.use("/:id/embeds", embedsRoute);
|
||||
notesRoute.use(uploadLimit);
|
||||
notesRoute.use(jsonParser);
|
||||
notesRoute.post("", postRateLimit, postNoteController);
|
||||
notesRoute.get("/:id", getRateLimit, getNoteController);
|
||||
notesRoute.delete("/:id", getRateLimit, deleteNoteController);
|
||||
notesRoute.route("/").post(postRateLimit, postNoteController);
|
||||
notesRoute.route("/:id").get(getRateLimit, getNoteController);
|
||||
|
@ -1,9 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
/**
|
||||
* Generates a 256 bit token using the nodeJS crypto module.
|
||||
* @returns base 64-encoded token.
|
||||
*/
|
||||
export function generateToken(): string {
|
||||
return crypto.randomBytes(32).toString("base64");
|
||||
}
|
4
server/src/db/__mocks__/embed.dao.ts
Normal file
4
server/src/db/__mocks__/embed.dao.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
export const getEmbed = vi.fn();
|
||||
export const createEmbed = vi.fn();
|
@ -1,7 +1,6 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
export const getNote = vi.fn();
|
||||
export const createNote = vi.fn();
|
||||
export const getExpiredNotes = vi.fn();
|
||||
export const deleteNotes = vi.fn();
|
||||
export const deleteNote = vi.fn();
|
||||
export const createNote = vi.fn();
|
||||
|
85
server/src/db/embed.dao.integration.test.ts
Normal file
85
server/src/db/embed.dao.integration.test.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { EncryptedNote } from "@prisma/client";
|
||||
import { getEmbed, createEmbed } from "./embed.dao";
|
||||
import { createNote } from "./note.dao";
|
||||
|
||||
const VALID_CIPHERTEXT = Buffer.from("sample_ciphertext").toString("base64");
|
||||
|
||||
describe("Reading and writing embeds", () => {
|
||||
it("Should write embeds for existing note", async () => {
|
||||
const note = await createNote({
|
||||
ciphertext: "test",
|
||||
hmac: "test",
|
||||
crypto_version: "v2",
|
||||
} as EncryptedNote);
|
||||
|
||||
const embed = {
|
||||
note_id: note.id,
|
||||
embed_id: "embed_id",
|
||||
hmac: "hmac",
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
};
|
||||
|
||||
const res = await createEmbed(embed);
|
||||
|
||||
expect(res.note_id).toEqual(embed.note_id);
|
||||
expect(res.embed_id).toEqual(embed.embed_id);
|
||||
expect(res.hmac).toEqual(embed.hmac);
|
||||
expect(res.id).not.toBeNull();
|
||||
expect(res.id.length).toBeGreaterThan(0);
|
||||
expect(res.ciphertext.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("Should throw if note_id does not refer to existing note", async () => {
|
||||
const embed = {
|
||||
note_id: "note_id",
|
||||
embed_id: "embed_id",
|
||||
hmac: "hmac",
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
};
|
||||
|
||||
await expect(createEmbed(embed)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("Should throw if embed_id is not unique", async () => {
|
||||
const note = await createNote({
|
||||
ciphertext: "test",
|
||||
hmac: "test",
|
||||
crypto_version: "v2",
|
||||
} as EncryptedNote);
|
||||
|
||||
const embed = {
|
||||
note_id: note.id,
|
||||
embed_id: "embed_id",
|
||||
hmac: "hmac",
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
};
|
||||
|
||||
await createEmbed(embed); // embed 1
|
||||
await expect(createEmbed(embed)).rejects.toThrowError(/Duplicate embed/g); // duplicate embed
|
||||
});
|
||||
|
||||
it("Should read embeds for existing note", async () => {
|
||||
const note = await createNote({
|
||||
ciphertext: "test",
|
||||
hmac: "test",
|
||||
crypto_version: "v2",
|
||||
} as EncryptedNote);
|
||||
|
||||
const embed = {
|
||||
note_id: note.id,
|
||||
embed_id: "embed_id",
|
||||
hmac: "hmac",
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
};
|
||||
|
||||
await createEmbed(embed);
|
||||
const res = await getEmbed(note.id, embed.embed_id);
|
||||
|
||||
expect(res).not.toBeNull();
|
||||
expect(res?.note_id).toEqual(embed.note_id);
|
||||
expect(res?.embed_id).toEqual(embed.embed_id);
|
||||
expect(res?.hmac).toEqual(embed.hmac);
|
||||
expect(res?.ciphertext).toEqual(embed.ciphertext);
|
||||
});
|
||||
});
|
68
server/src/db/embed.dao.ts
Normal file
68
server/src/db/embed.dao.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { EncryptedEmbed, Prisma, PrismaClient } from "@prisma/client";
|
||||
import { BufferToBase64, base64ToBuffer } from "../util";
|
||||
import prisma from "./client";
|
||||
|
||||
export interface EncryptedEmbedDTO {
|
||||
note_id: string;
|
||||
embed_id: string;
|
||||
ciphertext: string; // in base64
|
||||
hmac: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an embed for a note by embed_id.
|
||||
* @param noteId note id
|
||||
* @param embedId embed id
|
||||
* @returns encrypted embed (serialized ciphertext to base64)
|
||||
*/
|
||||
export async function getEmbed(
|
||||
noteId: string,
|
||||
embedId: string
|
||||
): Promise<EncryptedEmbedDTO | null> {
|
||||
const embed = await prisma.encryptedEmbed.findUnique({
|
||||
where: {
|
||||
noteId_embedId: {
|
||||
note_id: noteId,
|
||||
embed_id: embedId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!embed) return null;
|
||||
|
||||
return {
|
||||
note_id: embed.note_id,
|
||||
embed_id: embed.embed_id,
|
||||
hmac: embed.hmac,
|
||||
ciphertext: BufferToBase64(embed.ciphertext),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an embed for a note.
|
||||
* @param embed EncryptedEmbedDTO to serialize and save
|
||||
* @param transactionClient optionally pass a TransactionClient object when running in a Prisma interactive transaction
|
||||
* @returns the saved EncryptedEmbed (deserialized ciphertext to Buffer)
|
||||
*/
|
||||
export async function createEmbed(
|
||||
embed: EncryptedEmbedDTO,
|
||||
transactionClient: Prisma.TransactionClient = prisma
|
||||
): Promise<EncryptedEmbed> {
|
||||
const cipher_buf = base64ToBuffer(embed.ciphertext);
|
||||
const data = {
|
||||
note_id: embed.note_id,
|
||||
embed_id: embed.embed_id,
|
||||
hmac: embed.hmac,
|
||||
ciphertext: cipher_buf,
|
||||
size_bytes: cipher_buf.byteLength,
|
||||
} as EncryptedEmbed;
|
||||
return transactionClient.encryptedEmbed.create({ data }).catch((err) => {
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
// The .code property can be accessed in a type-safe manner
|
||||
if (err.code === "P2002") {
|
||||
throw new Error("Duplicate embed");
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
115
server/src/db/note.dao.integration.test.ts
Normal file
115
server/src/db/note.dao.integration.test.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getEmbed } from "./embed.dao";
|
||||
import { createNote, deleteNotes, getExpiredNotes, getNote } from "./note.dao";
|
||||
import prisma from "./client";
|
||||
|
||||
const VALID_CIPHERTEXT = Buffer.from("sample_ciphertext").toString("base64");
|
||||
const VALID_HMAC = Buffer.from("sample_hmac").toString("base64");
|
||||
|
||||
const VALID_NOTE = {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
crypto_version: "v2",
|
||||
expire_time: new Date(),
|
||||
} as EncryptedNote;
|
||||
|
||||
const VALID_EMBED = {
|
||||
embed_id: "embed_id",
|
||||
hmac: VALID_HMAC,
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
};
|
||||
|
||||
describe("Writes and reads", () => {
|
||||
it("should write a new note", async () => {
|
||||
const res = await createNote(VALID_NOTE);
|
||||
expect(res.id).not.toBeNull();
|
||||
expect(res.id.length).toBeGreaterThan(0);
|
||||
expect(res.ciphertext).toStrictEqual(VALID_NOTE.ciphertext);
|
||||
expect(res.hmac).toStrictEqual(VALID_NOTE.hmac);
|
||||
expect(res.crypto_version).toStrictEqual(VALID_NOTE.crypto_version);
|
||||
expect(res.expire_time).toStrictEqual(VALID_NOTE.expire_time);
|
||||
expect(res.insert_time).not.toBeNull();
|
||||
expect(res.insert_time.getTime()).toBeLessThanOrEqual(new Date().getTime());
|
||||
});
|
||||
|
||||
it("should write a new note with one embed", async () => {
|
||||
const res = await createNote(VALID_NOTE, [VALID_EMBED]);
|
||||
expect(res.id).not.toBeNull();
|
||||
|
||||
const res2 = await getEmbed(res.id, VALID_EMBED.embed_id);
|
||||
expect(res2).not.toBeNull();
|
||||
expect(res2?.ciphertext).toStrictEqual(VALID_EMBED.ciphertext);
|
||||
expect(res2?.hmac).toStrictEqual(VALID_EMBED.hmac);
|
||||
});
|
||||
|
||||
it("should write a new note with multiple embeds", async () => {
|
||||
const res = await createNote(VALID_NOTE, [
|
||||
VALID_EMBED,
|
||||
{ ...VALID_EMBED, embed_id: "embed_id2" },
|
||||
]);
|
||||
expect(res.id).not.toBeNull();
|
||||
|
||||
const res2 = await getEmbed(res.id, VALID_EMBED.embed_id);
|
||||
expect(res2?.embed_id).toStrictEqual(VALID_EMBED.embed_id);
|
||||
|
||||
const res3 = await getEmbed(res.id, "embed_id2");
|
||||
expect(res3?.embed_id).toStrictEqual("embed_id2");
|
||||
}),
|
||||
it("should fail writing a new note with duplicate embed_ids", async () => {
|
||||
await expect(
|
||||
createNote(VALID_NOTE, [VALID_EMBED, VALID_EMBED])
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it("should roll back a failed note with embeds", async () => {
|
||||
const noteCount = (await prisma.encryptedNote.findMany())?.length;
|
||||
const embedCount = (await prisma.encryptedEmbed.findMany())?.length;
|
||||
|
||||
await expect(
|
||||
createNote({ ...VALID_NOTE }, [VALID_EMBED, VALID_EMBED])
|
||||
).rejects.toThrowError();
|
||||
|
||||
const noteCountAfter = (await prisma.encryptedNote.findMany())?.length;
|
||||
const embedCountAfter = (await prisma.encryptedEmbed.findMany())?.length;
|
||||
|
||||
expect(noteCountAfter).toStrictEqual(noteCount);
|
||||
expect(embedCountAfter).toStrictEqual(embedCount);
|
||||
});
|
||||
|
||||
it("should find an existing note by id", async () => {
|
||||
const note = await createNote(VALID_NOTE);
|
||||
const res = await getNote(note.id);
|
||||
expect(res).not.toBeNull();
|
||||
expect(res).toMatchObject(note);
|
||||
});
|
||||
|
||||
it("should not find a non-existing note by id", async () => {
|
||||
const res = await getNote("non-existing-id");
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
it("should properly delete notes", async () => {
|
||||
const note = await createNote(VALID_NOTE);
|
||||
const res = await getNote(note.id);
|
||||
expect(res).not.toBeNull();
|
||||
const res2 = await deleteNotes([note.id]);
|
||||
expect(res2).toBe(1);
|
||||
const res3 = await getNote(note.id);
|
||||
expect(res3).toBeNull();
|
||||
});
|
||||
|
||||
it("should return expired notes", async () => {
|
||||
const expiredNote = await createNote({
|
||||
...VALID_NOTE,
|
||||
expire_time: new Date(0),
|
||||
});
|
||||
const freshNote = await createNote({
|
||||
...VALID_NOTE,
|
||||
expire_time: new Date(Date.now() + 1000),
|
||||
});
|
||||
const res = await getExpiredNotes();
|
||||
expect(res).toContainEqual(expiredNote);
|
||||
expect(res).not.toContainEqual(freshNote);
|
||||
});
|
||||
});
|
@ -1,5 +1,12 @@
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
import prisma from "./client";
|
||||
import { createEmbed, EncryptedEmbedDTO } from "./embed.dao";
|
||||
|
||||
type EncryptedEmbed = {
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
embed_id: string;
|
||||
};
|
||||
|
||||
export async function getNote(noteId: string): Promise<EncryptedNote | null> {
|
||||
return prisma.encryptedNote.findUnique({
|
||||
@ -7,9 +14,32 @@ export async function getNote(noteId: string): Promise<EncryptedNote | null> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function createNote(note: EncryptedNote): Promise<EncryptedNote> {
|
||||
return prisma.encryptedNote.create({
|
||||
data: note,
|
||||
export async function createNote(
|
||||
note: EncryptedNote,
|
||||
embeds: EncryptedEmbed[] = []
|
||||
): Promise<EncryptedNote> {
|
||||
return prisma.$transaction(async (transactionClient) => {
|
||||
// 1. Save note
|
||||
const savedNote = await transactionClient.encryptedNote.create({
|
||||
data: note,
|
||||
});
|
||||
|
||||
// 2. Store embeds
|
||||
if (embeds.length > 0) {
|
||||
const _embeds: EncryptedEmbedDTO[] = embeds.map(
|
||||
(embed) =>
|
||||
({
|
||||
...embed,
|
||||
note_id: savedNote.id,
|
||||
} as EncryptedEmbedDTO)
|
||||
);
|
||||
for (const embed of _embeds) {
|
||||
await createEmbed(embed, transactionClient);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Finalize transaction
|
||||
return savedNote;
|
||||
});
|
||||
}
|
||||
|
||||
@ -23,12 +53,6 @@ export async function getExpiredNotes(): Promise<EncryptedNote[]> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteNote(noteId: string): Promise<EncryptedNote> {
|
||||
return prisma.encryptedNote.delete({
|
||||
where: { id: noteId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteNotes(noteIds: string[]): Promise<number> {
|
||||
return prisma.encryptedNote
|
||||
.deleteMany({
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { crc16 as crc } from "crc";
|
||||
|
||||
/**
|
||||
* @param id {string} a 16 character base16 string with 12 random characters and 4 CRC characters
|
||||
* @returns {boolean} true if the id is valid, false otherwise
|
||||
*/
|
||||
export default function checkId(id: string): boolean {
|
||||
// check length
|
||||
if (id.length !== 16) {
|
||||
return false;
|
||||
}
|
||||
// extract the random number and the checksum
|
||||
const random = id.slice(0, 12);
|
||||
const checksum = id.slice(12, 16);
|
||||
|
||||
// compute the CRC of the random number
|
||||
const computedChecksum = crc(random).toString(16).padStart(4, "0");
|
||||
|
||||
// compare the computed checksum with the one in the id
|
||||
return computedChecksum === checksum;
|
||||
}
|
@ -1,24 +1,16 @@
|
||||
import { ScalableBloomFilter } from "bloom-filters";
|
||||
import { getFilter, upsertFilter } from "../db/bloomFilter.dao";
|
||||
|
||||
export const EXPIRED_NOTES_FILTER_NAME = "expiredNotes" as const;
|
||||
export const DELETED_NOTES_FILTER_NAME = "deletedNotes" as const;
|
||||
|
||||
type FilterName =
|
||||
| typeof EXPIRED_NOTES_FILTER_NAME
|
||||
| typeof DELETED_NOTES_FILTER_NAME;
|
||||
|
||||
export class NoteIdFilter {
|
||||
export class ExpiredNoteFilter {
|
||||
_filter: ScalableBloomFilter;
|
||||
_name: string;
|
||||
static FILTER_NAME = "expiredNotes";
|
||||
|
||||
private constructor(name: string, filter: ScalableBloomFilter) {
|
||||
private constructor(filter: ScalableBloomFilter) {
|
||||
this._filter = filter;
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
public static async deserializeFromDb(name: string): Promise<NoteIdFilter> {
|
||||
return NoteIdFilter._deserializeFilter(name)
|
||||
public static async deserializeFromDb(): Promise<ExpiredNoteFilter> {
|
||||
return ExpiredNoteFilter._deserializeFilter()
|
||||
.catch((err) => {
|
||||
if (err.message === "No BloomFilter found") {
|
||||
return new ScalableBloomFilter();
|
||||
@ -27,7 +19,7 @@ export class NoteIdFilter {
|
||||
}
|
||||
})
|
||||
.then((filter) => {
|
||||
return new NoteIdFilter(name, filter);
|
||||
return new ExpiredNoteFilter(filter);
|
||||
});
|
||||
}
|
||||
|
||||
@ -43,26 +35,24 @@ export class NoteIdFilter {
|
||||
}
|
||||
|
||||
private _serialize(): Promise<void> {
|
||||
return upsertFilter(this._name, this._filter);
|
||||
return upsertFilter(ExpiredNoteFilter.FILTER_NAME, this._filter);
|
||||
}
|
||||
|
||||
private static _deserializeFilter(
|
||||
name: string
|
||||
): Promise<ScalableBloomFilter> {
|
||||
return getFilter<ScalableBloomFilter>(name, ScalableBloomFilter);
|
||||
private static _deserializeFilter(): Promise<ScalableBloomFilter> {
|
||||
return getFilter<ScalableBloomFilter>(
|
||||
this.FILTER_NAME,
|
||||
ScalableBloomFilter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _filters: Record<FilterName, NoteIdFilter | null> = {
|
||||
expiredNotes: null,
|
||||
deletedNotes: null,
|
||||
};
|
||||
let _filter: ExpiredNoteFilter;
|
||||
|
||||
export async function getNoteFilter(name: FilterName): Promise<NoteIdFilter> {
|
||||
if (_filters[name] !== null) {
|
||||
return _filters[name] as NoteIdFilter;
|
||||
export async function getExpiredNoteFilter(): Promise<ExpiredNoteFilter> {
|
||||
if (_filter) {
|
||||
return _filter;
|
||||
} else {
|
||||
_filters[name] = await NoteIdFilter.deserializeFromDb(name);
|
||||
return _filters[name] as NoteIdFilter;
|
||||
_filter = await ExpiredNoteFilter.deserializeFromDb();
|
||||
return _filter;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { NoteIdFilter } from "./expiredNoteFilter";
|
||||
import { ExpiredNoteFilter } from "./expiredNoteFilter";
|
||||
import { ScalableBloomFilter } from "bloom-filters";
|
||||
|
||||
import * as dao from "../db/bloomFilter.dao";
|
||||
@ -16,12 +16,12 @@ describe("Deserialization from database", () => {
|
||||
mockedDao.getFilter.mockRejectedValue(new Error("No BloomFilter found"));
|
||||
|
||||
// test instatiation
|
||||
const testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
const testFilter = await ExpiredNoteFilter.deserializeFromDb();
|
||||
expect(mockedDao.getFilter).toHaveBeenCalledWith(
|
||||
"expiredNotes",
|
||||
ScalableBloomFilter
|
||||
);
|
||||
expect(testFilter).toBeInstanceOf(NoteIdFilter);
|
||||
expect(testFilter).toBeInstanceOf(ExpiredNoteFilter);
|
||||
|
||||
// expect the _filter property to be a fresh ScalableBloomFilter (capacity 8)
|
||||
expect(testFilter._filter).toBeInstanceOf(ScalableBloomFilter);
|
||||
@ -37,7 +37,7 @@ describe("Deserialization from database", () => {
|
||||
mockedDao.getFilter.mockResolvedValue(bloomFilter);
|
||||
|
||||
// test instatiation
|
||||
const testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
const testFilter = await ExpiredNoteFilter.deserializeFromDb();
|
||||
expect(mockedDao.getFilter).toHaveBeenCalledWith(
|
||||
"expiredNotes",
|
||||
ScalableBloomFilter
|
||||
@ -51,7 +51,7 @@ describe("Deserialization from database", () => {
|
||||
});
|
||||
|
||||
describe("Filter operations and serialization", () => {
|
||||
let testFilter: NoteIdFilter;
|
||||
let testFilter: ExpiredNoteFilter;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockedDao = vi.mocked(dao);
|
||||
@ -63,7 +63,7 @@ describe("Filter operations and serialization", () => {
|
||||
});
|
||||
|
||||
it("should add multiple noteIds to the filter", async () => {
|
||||
testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
testFilter = await ExpiredNoteFilter.deserializeFromDb();
|
||||
testFilter.addNoteIds(["test", "test2"]);
|
||||
expect(testFilter.hasNoteId("test")).toBe(true);
|
||||
expect(testFilter.hasNoteId("test2")).toBe(true);
|
||||
@ -77,7 +77,7 @@ describe("Filter operations and serialization", () => {
|
||||
});
|
||||
|
||||
it("Should have an error rate <1% for 1000 elements", async () => {
|
||||
testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
testFilter = await ExpiredNoteFilter.deserializeFromDb();
|
||||
const elements = Array.from({ length: 1000 }, (_, i) => i.toString());
|
||||
testFilter.addNoteIds(elements);
|
||||
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { event } from "@prisma/client";
|
||||
import prisma from "../db/client";
|
||||
import logger from "./logger";
|
||||
|
||||
export enum EventType {
|
||||
WRITE = "WRITE",
|
||||
READ = "READ",
|
||||
DELETE = "DELETE",
|
||||
UPDATE = "UPDATE",
|
||||
PURGE = "PURGE",
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
interface Event {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
@ -28,10 +25,6 @@ export interface WriteEvent extends ClientEvent {
|
||||
expire_window_days?: number;
|
||||
}
|
||||
|
||||
interface DeleteEvent extends ClientEvent {}
|
||||
|
||||
interface UpdateEvent extends ClientEvent {}
|
||||
|
||||
interface ReadEvent extends ClientEvent {}
|
||||
|
||||
interface PurgeEvent extends Event {
|
||||
@ -40,42 +33,19 @@ interface PurgeEvent extends Event {
|
||||
}
|
||||
|
||||
export default class EventLogger {
|
||||
private static printError(event: Event) {
|
||||
if (event.error) {
|
||||
logger.error(event.error);
|
||||
}
|
||||
}
|
||||
|
||||
public static writeEvent(event: WriteEvent): Promise<event> {
|
||||
this.printError(event);
|
||||
return prisma.event.create({
|
||||
data: { type: EventType.WRITE, ...event },
|
||||
});
|
||||
}
|
||||
|
||||
public static readEvent(event: ReadEvent): Promise<event> {
|
||||
this.printError(event);
|
||||
return prisma.event.create({
|
||||
data: { type: EventType.READ, ...event },
|
||||
});
|
||||
}
|
||||
|
||||
public static deleteEvent(event: DeleteEvent): Promise<event> {
|
||||
this.printError(event);
|
||||
return prisma.event.create({
|
||||
data: { type: EventType.DELETE, ...event },
|
||||
});
|
||||
}
|
||||
|
||||
public static updateEvent(event: UpdateEvent): Promise<event> {
|
||||
this.printError(event);
|
||||
return prisma.event.create({
|
||||
data: { type: EventType.UPDATE, ...event },
|
||||
});
|
||||
}
|
||||
|
||||
public static purgeEvent(event: PurgeEvent): Promise<event> {
|
||||
this.printError(event);
|
||||
return prisma.event.create({
|
||||
data: { type: EventType.PURGE, ...event },
|
||||
});
|
||||
|
@ -1,19 +1,9 @@
|
||||
import { vi } from "vitest";
|
||||
import { Event } from "../EventLogger";
|
||||
import logger from "../logger";
|
||||
|
||||
const logEventToConsole = (event: Event) => {
|
||||
if (event.error) {
|
||||
console.error(event.error);
|
||||
}
|
||||
};
|
||||
|
||||
const mockedEventLogger = {
|
||||
writeEvent: vi.fn(logEventToConsole),
|
||||
readEvent: vi.fn(logEventToConsole),
|
||||
purgeEvent: vi.fn(logEventToConsole),
|
||||
deleteEvent: vi.fn(logEventToConsole),
|
||||
updateEvent: vi.fn(logEventToConsole),
|
||||
writeEvent: vi.fn(),
|
||||
readEvent: vi.fn(),
|
||||
purgeEvent: vi.fn(),
|
||||
};
|
||||
|
||||
export default mockedEventLogger;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { deleteNotes, getExpiredNotes } from "../db/note.dao";
|
||||
import { getNoteFilter } from "../lib/expiredNoteFilter";
|
||||
import { getExpiredNoteFilter } from "../lib/expiredNoteFilter";
|
||||
import EventLogger from "../logging/EventLogger";
|
||||
import logger from "../logging/logger";
|
||||
import { getNoteSize } from "../util";
|
||||
|
||||
export async function deleteExpiredNotes(): Promise<number> {
|
||||
logger.info("[Cleanup] Cleaning up expired notes...");
|
||||
@ -12,18 +11,18 @@ export async function deleteExpiredNotes(): Promise<number> {
|
||||
.then(async (deleteCount) => {
|
||||
const logs = toDelete.map(async (note) => {
|
||||
logger.info(
|
||||
`[Cleanup] Deleted note ${note.id} with size ${getNoteSize(
|
||||
note
|
||||
)} bytes`
|
||||
`[Cleanup] Deleted note ${note.id} with size ${
|
||||
note.ciphertext.length + note.hmac.length
|
||||
} bytes`
|
||||
);
|
||||
return EventLogger.purgeEvent({
|
||||
success: true,
|
||||
note_id: note.id,
|
||||
size_bytes: getNoteSize(note),
|
||||
size_bytes: note.ciphertext.length + note.hmac.length,
|
||||
});
|
||||
});
|
||||
await Promise.all(logs);
|
||||
const filter = await getNoteFilter("expiredNotes");
|
||||
const filter = await getExpiredNoteFilter();
|
||||
await filter.addNoteIds(toDelete.map((n) => n.id));
|
||||
logger.info(`[Cleanup] Deleted ${deleteCount} expired notes.`);
|
||||
return deleteCount;
|
||||
|
@ -11,7 +11,7 @@ vi.mock("../lib/expiredNoteFilter", () => {
|
||||
const instance = {
|
||||
addNoteIds: vi.fn(),
|
||||
};
|
||||
return { getNoteFilter: (name: string) => instance };
|
||||
return { getExpiredNoteFilter: () => instance };
|
||||
});
|
||||
|
||||
vi.spyOn(logger, "error");
|
||||
@ -29,17 +29,15 @@ describe("deleteExpiredNotes", () => {
|
||||
id: "test",
|
||||
ciphertext: "test",
|
||||
hmac: "test",
|
||||
iv: null,
|
||||
insert_time: new Date(),
|
||||
expire_time: new Date(),
|
||||
crypto_version: "v1",
|
||||
secret_token: "secret_token",
|
||||
},
|
||||
]);
|
||||
mockedDao.deleteNotes.mockResolvedValue(1);
|
||||
|
||||
// mock ExpiredNoteFilter
|
||||
const mockedFilter = vi.mocked(await filter.getNoteFilter("expiredNotes"));
|
||||
const mockedFilter = vi.mocked(await filter.getExpiredNoteFilter());
|
||||
mockedFilter.addNoteIds.mockResolvedValue();
|
||||
|
||||
// test task call
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
|
||||
export function addDays(date: Date, days: number): Date {
|
||||
@ -13,10 +12,12 @@ export function getConnectingIp(req: Request): string {
|
||||
req.socket.remoteAddress) as string;
|
||||
}
|
||||
|
||||
export function getNoteSize(
|
||||
note: Pick<EncryptedNote, "ciphertext" | "hmac" | "iv">
|
||||
) {
|
||||
return (
|
||||
note.ciphertext.length + (note.hmac?.length ?? 0) + (note.iv?.length ?? 0)
|
||||
);
|
||||
// base64 to array buffer (Node JS api, so don't use atob or btoa)
|
||||
export function base64ToBuffer(base64: string): Buffer {
|
||||
return Buffer.from(base64, "base64");
|
||||
}
|
||||
|
||||
// array buffer to base64 (Node JS api, so don't use atob or btoa)
|
||||
export function BufferToBase64(buffer: Buffer): string {
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { addDays, getConnectingIp } from "./util";
|
||||
import {
|
||||
addDays,
|
||||
BufferToBase64,
|
||||
base64ToBuffer,
|
||||
getConnectingIp,
|
||||
} from "./util";
|
||||
|
||||
describe("addDays()", () => {
|
||||
it("Should add n days to the input date", () => {
|
||||
@ -8,3 +13,19 @@ describe("addDays()", () => {
|
||||
expect(addDays(date, 30)).toEqual(expectedDate);
|
||||
});
|
||||
});
|
||||
|
||||
describe("converting to/from base64", () => {
|
||||
it("Should convert a base64 string to an array buffer", () => {
|
||||
const base64 = "EjRWeJA=";
|
||||
const expectedBuffer = new Uint8Array([18, 52, 86, 120, 144]);
|
||||
expect(new Uint8Array(base64ToBuffer(base64))).toStrictEqual(
|
||||
expectedBuffer
|
||||
);
|
||||
});
|
||||
|
||||
it("Should convert an array buffer to a base64 string", () => {
|
||||
const buffer = new Uint8Array([18, 52, 86, 120, 144]);
|
||||
const expectedBase64 = "EjRWeJA=";
|
||||
expect(BufferToBase64(buffer)).toEqual(expectedBase64);
|
||||
});
|
||||
});
|
||||
|
@ -1,40 +0,0 @@
|
||||
import {
|
||||
IsBase64,
|
||||
IsHexadecimal,
|
||||
IsNotEmpty,
|
||||
Matches,
|
||||
ValidateIf,
|
||||
} from "class-validator";
|
||||
|
||||
abstract class NoteRequestBody {
|
||||
@ValidateIf((o) => o.user_id != null)
|
||||
@IsHexadecimal()
|
||||
user_id: string | undefined;
|
||||
|
||||
@ValidateIf((o) => o.plugin_version != null)
|
||||
@Matches("^[0-9]+\\.[0-9]+\\.[0-9]+$")
|
||||
plugin_version: string | undefined;
|
||||
}
|
||||
|
||||
export class NotePostRequest extends NoteRequestBody {
|
||||
@IsBase64()
|
||||
@IsNotEmpty()
|
||||
ciphertext: string | undefined;
|
||||
|
||||
@IsBase64()
|
||||
@ValidateIf((o) => !o.iv)
|
||||
hmac?: string | undefined;
|
||||
|
||||
@IsBase64()
|
||||
@ValidateIf((o) => !o.hmac)
|
||||
iv?: string | undefined;
|
||||
|
||||
@Matches("^v[0-9]+$")
|
||||
crypto_version: string = "v1";
|
||||
}
|
||||
|
||||
export class NoteDeleteRequest extends NoteRequestBody {
|
||||
@IsBase64()
|
||||
@IsNotEmpty()
|
||||
secret_token: string | undefined;
|
||||
}
|
3
webapp/.gitignore
vendored
3
webapp/.gitignore
vendored
@ -1,8 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit/*
|
||||
!/.svelte-kit/tsconfig.json
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
|
@ -1,46 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
"paths": {
|
||||
"$lib": [
|
||||
"src/lib"
|
||||
],
|
||||
"$lib/*": [
|
||||
"src/lib/*"
|
||||
]
|
||||
},
|
||||
"rootDirs": [
|
||||
"..",
|
||||
"./types"
|
||||
],
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"preserveValueImports": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"module": "esnext",
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": [
|
||||
"ambient.d.ts",
|
||||
"./types/**/$types.d.ts",
|
||||
"../vite.config.ts",
|
||||
"../src/**/*.js",
|
||||
"../src/**/*.ts",
|
||||
"../src/**/*.svelte",
|
||||
"../src/**/*.js",
|
||||
"../src/**/*.ts",
|
||||
"../src/**/*.svelte",
|
||||
"../tests/**/*.js",
|
||||
"../tests/**/*.ts",
|
||||
"../tests/**/*.svelte"
|
||||
],
|
||||
"exclude": [
|
||||
"../node_modules/**",
|
||||
"./[!ambient.d.ts]**"
|
||||
]
|
||||
}
|
@ -1,24 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [2022-11-14] (["HACK WEEK"](https://mcndt.dev/posts/hack-week-november-2022/))
|
||||
|
||||
- feat: ✨ Support for showing file title as note title in web viewer.
|
||||
|
||||
## [2022-11-13] (["HACK WEEK"](https://mcndt.dev/posts/hack-week-november-2022/))
|
||||
|
||||
- security: 🔐 Can now decrypt GCM-encrypted notes from Quickshare plugin versions 1.0.2 and higher.
|
||||
|
||||
## [2022-09-11]
|
||||
|
||||
- fix: 🐛 Fix inline code showing the backtick syntax after rendering.
|
||||
|
||||
## [2022-08-23]
|
||||
|
||||
- feat: ✨ Footnotes are rendered as they are in the Obsidian client.
|
||||
|
||||
## [2022-08-16]
|
||||
|
||||
- fix: 🐛 Fix highlights not rendering correctly when mixed with other formatting. ([issue #19](https://github.com/mcndt/noteshare.space/issues/19))
|
||||
- fix: 🐛 Fix highlights not rendering correctly when mixed with other formatting. ([issue #19](https://github.com/mcndt/noteshare.space/issues/19))
|
||||
- fix: 🐛 Fix some characters escaping the rendering for #tags. ([issue #10](https://github.com/mcndt/noteshare.space/issues/10))
|
||||
|
||||
## [2022-08-11]
|
||||
|
1379
webapp/package-lock.json
generated
1379
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,9 +5,11 @@
|
||||
"author": "Maxime Cannoodt (mcndt)",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "svelte-kit preview",
|
||||
"prepare": "svelte-kit sync",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||
@ -18,10 +20,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/kit": "^1.0.0-next.544",
|
||||
"@sveltejs/kit": "^1.0.0-next.350",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@testing-library/svelte": "^3.1.3",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/marked": "^4.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
@ -41,13 +43,13 @@
|
||||
"tailwindcss": "^3.1.3",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.7.2",
|
||||
"vite": "^3.2.3",
|
||||
"vite-plugin-markdown": "^2.1.0",
|
||||
"vitest": "^0.17.0"
|
||||
"vitest": "^0.17.0",
|
||||
"vitest-svelte-kit": "^0.0.6"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.100",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.78",
|
||||
"crypto-js": "^4.1.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"katex": "^0.16.0",
|
||||
|
3842
webapp/pnpm-lock.yaml
generated
3842
webapp/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,8 @@
|
||||
import { getCalloutColor, getCalloutIcon } from '$lib/util/callout';
|
||||
import CalloutIcon from '$lib/components/CalloutIcon.svelte';
|
||||
|
||||
export let title = '';
|
||||
export let type = 'note';
|
||||
let title = '';
|
||||
let type = 'note';
|
||||
let color = '--callout-warning';
|
||||
let icon = 'note';
|
||||
let init = false;
|
||||
@ -12,9 +12,8 @@
|
||||
|
||||
$: if (content) {
|
||||
const titleElement = content.getElementsByTagName('p')[0];
|
||||
const preFilled = title != '';
|
||||
const match = titleElement.innerText.split('\n')[0].match(/\[!(.+)\]([+-]?)(?:\s(.+))?/);
|
||||
if (match && !preFilled) {
|
||||
if (match) {
|
||||
type = match[1]?.trim();
|
||||
title = match[3]?.trim() ?? type[0].toUpperCase() + type.substring(1).toLowerCase();
|
||||
}
|
||||
@ -23,13 +22,11 @@
|
||||
icon = getCalloutIcon(type);
|
||||
|
||||
// Remove title from content
|
||||
if (!preFilled) {
|
||||
const pos = titleElement.innerHTML.indexOf('<br>');
|
||||
if (pos >= 0) {
|
||||
titleElement.innerHTML = titleElement.innerHTML.substring(pos + 4);
|
||||
} else {
|
||||
titleElement.innerHTML = '';
|
||||
}
|
||||
const pos = titleElement.innerHTML.indexOf('<br>');
|
||||
if (pos >= 0) {
|
||||
titleElement.innerHTML = titleElement.innerHTML.substring(pos + 4);
|
||||
} else {
|
||||
titleElement.innerHTML = '';
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import Callout from './Callout.svelte';
|
||||
|
||||
const localStorageKey = 'shared-note-notification';
|
||||
// Increase this value to show the notification again to existing users.
|
||||
const messageId = 3;
|
||||
// Use this to show the notification to new users only if the notification is younger than this date.
|
||||
const expire_time = new Date('2022-09-10');
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (!browser) return;
|
||||
const serializedId = localStorage.getItem(localStorageKey);
|
||||
const id = serializedId ? parseInt(serializedId) : -1;
|
||||
if (id < messageId && new Date() < expire_time) {
|
||||
show = true;
|
||||
}
|
||||
});
|
||||
|
||||
function onDismiss() {
|
||||
show = false;
|
||||
localStorage.setItem(localStorageKey, messageId.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<!-- <div class="mb-5 px-4 py-4 bg-blue-100 rounded-lg"> -->
|
||||
<div
|
||||
class="prose prose-zinc dark:prose-invert max-w-none prose-li:my-0 prose-ul:mt-0 prose-ol:mt-0 leading-7
|
||||
prose-strong:font-bold prose-a:font-normal prose-blockquote:font-normal prose-blockquote:not-italic
|
||||
prose-blockquote:first:before:content-[''] prose-hr:transition-colors mb-5"
|
||||
>
|
||||
<Callout type="info" title="Obsidian QuickShare 1.0.0 launched 🚀">
|
||||
<p>
|
||||
Obsidian QuickShare and Noteshare.space are now out of beta 🚀 You can now find the plugin
|
||||
in the Obsidian community plugin marketplace (see <a href="/install">instructions</a>).
|
||||
Check out the roadmap for upcoming features <a href="/roadmap">here</a>.
|
||||
</p>
|
||||
<div class="mt-1.5">
|
||||
<button
|
||||
on:click={onDismiss}
|
||||
class="px-1.5 py-0.5 text-[.9em] hover:bg-neutral-200 dark:hover:bg-neutral-700 font-semibold text-red-700 dark:text-red-500 underline hover:text-grey-500"
|
||||
>Don't show again</button
|
||||
>
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
{/if}
|
@ -1,7 +1,7 @@
|
||||
<hr class="border-zinc-200 dark:border-zinc-700 transition-colors" />
|
||||
|
||||
<footer
|
||||
class="px-3 py-6 md:p-8 text-center flex flex-wrap justify-center items-center gap-x-2 gap-y-1.5 text-zinc-500 dark:text-zinc-400"
|
||||
class="px-3 py-6 md:p-8 text-center flex flex-wrap justify-center items-center gap-x-2 text-zinc-500 dark:text-zinc-400"
|
||||
>
|
||||
<span>
|
||||
Built with love by <a class="underline" href="https://mcndt.dev" alt="blog">mcndt</a>
|
||||
@ -11,8 +11,6 @@
|
||||
<span>-</span>
|
||||
<a class="underline" href="/changelog">Changelog</a>
|
||||
<span>-</span>
|
||||
<a class="underline" href="/roadmap">Roadmap</a>
|
||||
<span>-</span>
|
||||
<a class="underline" href="/contact">Contact</a>
|
||||
<span>-</span>
|
||||
<a class="underline" href="https://discord.gg/y3HqyGeABK">Discord</a>
|
||||
@ -23,7 +21,5 @@
|
||||
>🐛 Report bug</a
|
||||
>
|
||||
<span>-</span>
|
||||
<a class="underline" href="/funding">Expenses & funding</a>
|
||||
<span>-</span>
|
||||
<a class="underline" href="https://www.buymeacoffee.com/mcndt">☕ Buy me a coffee</a>
|
||||
</footer>
|
||||
|
@ -19,8 +19,6 @@
|
||||
import Footnote from '$lib/marked/renderers/Footnote.svelte';
|
||||
|
||||
export let plaintext: string;
|
||||
export let fileTitle: string | undefined;
|
||||
|
||||
let ref: HTMLDivElement;
|
||||
let footnotes: HTMLDivElement[];
|
||||
let footnoteContainer: HTMLDivElement;
|
||||
@ -31,14 +29,10 @@
|
||||
const options = { ...marked.defaults, breaks: true };
|
||||
|
||||
function onParsed() {
|
||||
!fileTitle && setTitle();
|
||||
setTitle();
|
||||
parseFootnotes();
|
||||
}
|
||||
|
||||
$: if (fileTitle) {
|
||||
document.title = fileTitle.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for the first major header in the document to use as page title.
|
||||
*/
|
||||
@ -72,11 +66,8 @@
|
||||
id="md-box"
|
||||
class="prose prose-zinc dark:prose-invert max-w-none prose-li:my-0 prose-ul:mt-0 prose-ol:mt-0 leading-7
|
||||
prose-strong:font-bold prose-a:font-normal prose-blockquote:font-normal prose-blockquote:not-italic
|
||||
prose-blockquote:first:before:content-[''] prose-hr:transition-colors prose-code:before:content-[''] prose-code:after:content-['']"
|
||||
prose-blockquote:first:before:content-[''] prose-hr:transition-colors"
|
||||
>
|
||||
{#if fileTitle}
|
||||
<h1>{fileTitle}</h1>
|
||||
{/if}
|
||||
<SvelteMarkdown
|
||||
on:parsed={onParsed}
|
||||
renderers={{
|
||||
|
@ -2,31 +2,16 @@
|
||||
|
||||
import { AES, enc, HmacSHA256 } from 'crypto-js';
|
||||
|
||||
type CryptData = {
|
||||
ciphertext: string;
|
||||
key: string;
|
||||
iv?: string;
|
||||
hmac?: string;
|
||||
};
|
||||
|
||||
type CryptData_v1 = CryptData & {
|
||||
hmac: string;
|
||||
};
|
||||
|
||||
type CryptData_v3 = CryptData & {
|
||||
iv: string;
|
||||
};
|
||||
|
||||
export async function decrypt(cryptData: CryptData, version: string): Promise<string> {
|
||||
export async function decrypt(
|
||||
cryptData: { ciphertext: string; hmac: string; key: string },
|
||||
version: string
|
||||
): Promise<string> {
|
||||
console.debug(`decrypting with crypto suite ${version}`);
|
||||
if (version === 'v1') {
|
||||
return decrypt_v1(cryptData as CryptData_v1);
|
||||
return decrypt_v1(cryptData);
|
||||
}
|
||||
if (version === 'v2') {
|
||||
return decrypt_v2(cryptData as CryptData_v1);
|
||||
}
|
||||
if (version === 'v3') {
|
||||
return decrypt_v3(cryptData as CryptData_v3);
|
||||
return decrypt_v2(cryptData);
|
||||
}
|
||||
throw new Error(`Unsupported crypto version: ${version}`);
|
||||
}
|
||||
@ -51,6 +36,15 @@ export async function decrypt_v2(cryptData: {
|
||||
hmac: string;
|
||||
key: string;
|
||||
}): Promise<string> {
|
||||
const md = await decryptBuffer_v2(cryptData);
|
||||
return new TextDecoder().decode(md);
|
||||
}
|
||||
|
||||
export async function decryptBuffer_v2(cryptData: {
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
key: string;
|
||||
}): Promise<ArrayBuffer> {
|
||||
const secret = base64ToArrayBuffer(cryptData.key);
|
||||
const ciphertext_buf = base64ToArrayBuffer(cryptData.ciphertext);
|
||||
const hmac_buf = base64ToArrayBuffer(cryptData.hmac);
|
||||
@ -66,39 +60,17 @@ export async function decrypt_v2(cryptData: {
|
||||
throw Error('Failed HMAC check');
|
||||
}
|
||||
|
||||
const md = await window.crypto.subtle.decrypt(
|
||||
const data = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv: new Uint8Array(16) },
|
||||
await _getAesCbcKey(secret),
|
||||
await _getAesKey(secret),
|
||||
ciphertext_buf
|
||||
);
|
||||
return new TextDecoder().decode(md);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function decrypt_v3(cryptData: {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
key: string;
|
||||
}): Promise<string> {
|
||||
const secret = base64ToArrayBuffer(cryptData.key);
|
||||
const ciphertext_buf = base64ToArrayBuffer(cryptData.ciphertext);
|
||||
const iv_buf = base64ToArrayBuffer(cryptData.iv);
|
||||
|
||||
const md = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: iv_buf },
|
||||
await _getAesGcmKey(secret),
|
||||
ciphertext_buf
|
||||
);
|
||||
return new TextDecoder().decode(md);
|
||||
}
|
||||
|
||||
function _getAesCbcKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
function _getAesKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.importKey('raw', secret, { name: 'AES-CBC', length: 256 }, false, [
|
||||
'decrypt'
|
||||
]);
|
||||
}
|
||||
|
||||
function _getAesGcmKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.importKey('raw', secret, { name: 'AES-GCM', length: 256 }, false, [
|
||||
'encrypt',
|
||||
'decrypt'
|
||||
]);
|
||||
}
|
||||
|
34
webapp/src/lib/crypto/embedId.ts
Normal file
34
webapp/src/lib/crypto/embedId.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export async function getEmbedId(filename: string): Promise<string> {
|
||||
// generate 64 bit id
|
||||
const idBuf = new Uint32Array((await deriveKey(filename)).slice(0, 8));
|
||||
|
||||
// convert idBuf to base 32 string
|
||||
const id = idBuf.reduce((acc, cur) => {
|
||||
return acc + cur.toString(32);
|
||||
}, '');
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async function deriveKey(seed: string): Promise<ArrayBuffer> {
|
||||
const keyMaterial = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(seed),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits']
|
||||
);
|
||||
|
||||
const masterKey = await window.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: new Uint8Array(16),
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
return new Uint8Array(masterKey);
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
@ -10,14 +12,12 @@
|
||||
let highlighted: string;
|
||||
|
||||
onMount(() => {
|
||||
try {
|
||||
if (browser) {
|
||||
if (hljs.getLanguage(lang) !== undefined) {
|
||||
highlighted = hljs.highlight(text, { language: lang }).value;
|
||||
} else {
|
||||
highlighted = text;
|
||||
}
|
||||
} catch {
|
||||
highlighted = text;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,25 +1,71 @@
|
||||
<script lang="ts">
|
||||
import EmbedIcon from 'svelte-icons/md/MdAttachment.svelte';
|
||||
import FaRegQuestionCircle from 'svelte-icons/fa/FaRegQuestionCircle.svelte';
|
||||
import { EmbedType, getEmbedType, getMimeType } from '$lib/util/embeds';
|
||||
import { onMount } from 'svelte';
|
||||
import { getEmbedId } from '$lib/crypto/embedId';
|
||||
import type { EncryptedEmbed } from '$lib/model/EncryptedEmbed';
|
||||
import { decryptBuffer_v2 } from '$lib/crypto/decrypt';
|
||||
|
||||
export let text: string;
|
||||
let image: HTMLImageElement;
|
||||
let imageUrl: string;
|
||||
|
||||
onMount(async () => {
|
||||
if (getEmbedType(text) === EmbedType.IMAGE) {
|
||||
const encryptedEmbed = await fetchEmbed(text);
|
||||
const embedBuffer = await decryptEmbed(encryptedEmbed);
|
||||
console.log(embedBuffer);
|
||||
imageUrl = renderImage(embedBuffer, text);
|
||||
return () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function renderImage(buffer: ArrayBuffer, filename: string): string {
|
||||
// const bufferView = new Uint8Array(buffer);
|
||||
const blob = new Blob([buffer], { type: getMimeType(filename) });
|
||||
const url = URL.createObjectURL(blob);
|
||||
return url;
|
||||
}
|
||||
|
||||
async function decryptEmbed(embed: EncryptedEmbed): Promise<ArrayBuffer> {
|
||||
const key = location.hash.slice(1);
|
||||
const data = await decryptBuffer_v2({ ...embed, key });
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchEmbed(filename: string): Promise<EncryptedEmbed> {
|
||||
const embedId = await getEmbedId(filename);
|
||||
const response = await fetch(`${location.pathname}/embeds/${embedId}`);
|
||||
if (response.ok) {
|
||||
return (await response.json()) as EncryptedEmbed;
|
||||
}
|
||||
throw new Error(`Failed to fetch embed: ${response.statusText}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<dfn class="not-italic" title="Interal embeds are not shared currently.">
|
||||
<div
|
||||
class="px-4 py-12 border border-zinc-300 dark:border-zinc-600 inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
<span class="h-8 text-zinc-400 ml-0.5 inline-flex items-center whitespace-nowrap gap-1"
|
||||
><span class="w-8 h-8 inline-block">
|
||||
<EmbedIcon />
|
||||
{#if imageUrl}
|
||||
<img bind:this={image} src={imageUrl} alt={text} />
|
||||
{:else}
|
||||
<div>
|
||||
<dfn class="not-italic" title="Interal embeds are not shared currently.">
|
||||
<div
|
||||
class="px-4 py-12 border border-zinc-300 dark:border-zinc-600 inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
<span class="h-8 text-zinc-400 ml-0.5 inline-flex items-center whitespace-nowrap gap-1"
|
||||
><span class="w-8 h-8 inline-block">
|
||||
<EmbedIcon />
|
||||
</span>
|
||||
<span>Internal embed</span>
|
||||
</span>
|
||||
<span>Internal embed</span>
|
||||
</span>
|
||||
<span class="underline cursor-not-allowed inline-flex items-center">
|
||||
<span class="text-[#705dcf] opacity-50">{text}</span>
|
||||
<span class="inline-block w-3 h-3 mb-2 text-zinc-400 ml-0.5"><FaRegQuestionCircle /></span>
|
||||
</span>
|
||||
</div>
|
||||
</dfn>
|
||||
</div>
|
||||
<span class="underline cursor-not-allowed inline-flex items-center">
|
||||
<span class="text-[#705dcf] opacity-50">{text}</span>
|
||||
<span class="inline-block w-3 h-3 mb-2 text-zinc-400 ml-0.5"><FaRegQuestionCircle /></span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</dfn>
|
||||
</div>
|
||||
{/if}
|
||||
|
6
webapp/src/lib/model/EncryptedEmbed.ts
Normal file
6
webapp/src/lib/model/EncryptedEmbed.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type EncryptedEmbed = {
|
||||
note_id: string;
|
||||
embed_id: string;
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
};
|
21
webapp/src/lib/util/embeds.ts
Normal file
21
webapp/src/lib/util/embeds.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Returns the EmbedType if embeddable, false if not.
|
||||
* @param filename File extension to check.
|
||||
* @returns EmbedType if embeddable, false if not.
|
||||
*/
|
||||
export function getEmbedType(filename: string): EmbedType | boolean {
|
||||
return isImage(filename) ? EmbedType.IMAGE : false;
|
||||
}
|
||||
|
||||
export function getMimeType(filename: string) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
|
||||
export enum EmbedType {
|
||||
IMAGE = 'IMAGE'
|
||||
}
|
||||
|
||||
function isImage(filename: string): boolean {
|
||||
const match = filename.match(/(png|jpe?g|svg|bmp|gif|)$/i);
|
||||
return !!match && match[0]?.length > 0;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
|
||||
console.log($page);
|
||||
</script>
|
||||
|
||||
<div class="prose max-w-2xl prose-zinc dark:prose-invert">
|
||||
{#if $page.status === 404}
|
||||
<h1>404: No note found 🕵️</h1>
|
||||
<p class="prose-xl">No note was found at this link. Are you from the future?</p>
|
||||
{:else if $page.status === 410 && $page.error?.message === 'Note expired'}
|
||||
<h1>📝💨 This note is no longer here!</h1>
|
||||
<p class="prose-xl">
|
||||
Notes are stored for a limited amount of time. The note at this link was either set to expire,
|
||||
or deleted due to inactivity. Sorry!
|
||||
</p>
|
||||
{:else if $page.status === 410 && $page.error?.message === 'Note deleted'}
|
||||
<h1>📝🗑 This note has been deleted.</h1>
|
||||
<p class="prose-xl">The note at this link has been deleted by the user who shared it. Sorry!</p>
|
||||
{:else}
|
||||
<h1>Something went wrong 🤔</h1>
|
||||
<p class="prose-xl">
|
||||
{#if import.meta.env.DEV}
|
||||
<pre class="prose-xl">{JSON.stringify($page.error, null, 2)}</pre>
|
||||
{:else}
|
||||
<p class="prose-xl">An error occurred while loading this page. Please try again later.</p>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="not-prose w-full flex justify-center mt-16">
|
||||
{#if $page.status === 404 || ($page.status === 410 && $page.error?.message === 'Note expired')}
|
||||
<img src="/expired_note.svg" alt="encrypted-art" class="w-80" />
|
||||
{:else if $page.status === 410 && $page.error?.message === 'Note deleted'}
|
||||
<img src="/deleted_note.svg" alt="encrypted-art" class="w-80" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
@ -1,2 +0,0 @@
|
||||
// try making this false to prevent LinkedIn crawler returning 416
|
||||
export const prerender = false;
|
43
webapp/src/routes/__error.svelte
Normal file
43
webapp/src/routes/__error.svelte
Normal file
@ -0,0 +1,43 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = ({ error, status }) => {
|
||||
let explainText = '';
|
||||
let title = '';
|
||||
|
||||
if (status == 404) {
|
||||
title = `404: No note found 🕵️`;
|
||||
explainText = `No note was found at this link. Are you from the future?`;
|
||||
}
|
||||
|
||||
if (status == 410) {
|
||||
title = '📝💨 This note is no longer here! ';
|
||||
explainText = `Notes are stored for a limited amount of time. The note at this link was either set to expire, or deleted due to inactivity. Sorry!`;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
status: status,
|
||||
title: title,
|
||||
explainText: explainText
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let status: number;
|
||||
export let title: string;
|
||||
export let explainText: string;
|
||||
</script>
|
||||
|
||||
<div class="prose max-w-2xl prose-zinc dark:prose-invert">
|
||||
<h1>{title}</h1>
|
||||
<p class="prose-xl">{explainText}</p>
|
||||
|
||||
<div class="not-prose w-full flex justify-center mt-16">
|
||||
{#if status == 404 || status == 410}
|
||||
<img src="/expired_note.svg" alt="encrypted-art" class="w-80" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import NavBar from '$lib/components/navbar/NavBar.svelte';
|
||||
@ -29,42 +29,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{import.meta.env.VITE_BRANDING} — Securely share your Obsidian notes with one click.</title
|
||||
>
|
||||
<meta
|
||||
name="title"
|
||||
content="Noteshare.space — Securely share your Obsidian notes with one click."
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Securely share your Obsidian notes with one click. Zero configuration. End-to-end encrypted. No account needed. Completely open source! Download the QuickShare extension in the Obsidian community plugin marketplace."
|
||||
/>
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://noteshare.space/" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Noteshare.space — Securely share your Obsidian notes with one click."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Securely share your Obsidian notes with one click. Zero configuration. End-to-end encrypted. No account needed. Completely open source! Download the QuickShare extension in the Obsidian community plugin marketplace."
|
||||
/>
|
||||
<meta property="og:image" content="https://noteshare.space/meta.png" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://noteshare.space/" />
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content="Noteshare.space — Securely share your Obsidian notes with one click."
|
||||
/>
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Securely share your Obsidian notes with one click. Zero configuration. End-to-end encrypted. No account needed. Completely open source! Download the QuickShare extension in the Obsidian community plugin marketplace."
|
||||
/>
|
||||
<meta property="twitter:image" content="https://noteshare.space/meta.png" />
|
||||
<title>{import.meta.env.VITE_BRANDING}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class=" h-full {dark !== undefined ? '' : 'hidden'} {dark ? darkTheme : ''}">
|
||||
@ -91,6 +56,7 @@
|
||||
>
|
||||
</span>
|
||||
</NavBarLink>
|
||||
|
||||
<ThemeToggle bind:dark />
|
||||
</svelte:fragment>
|
||||
></NavBar
|
@ -1,3 +1,7 @@
|
||||
<script context="module">
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>About | {import.meta.env.VITE_BRANDING}</title>
|
||||
</svelte:head>
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = true;
|
||||
|
||||
// @ts-expect-error - Markdown files are not recognized by Svelte
|
||||
import { html } from '../funding.md';
|
||||
import { toc, html } from '/CHANGELOG.md';
|
||||
</script>
|
||||
|
||||
<div class="md:py-8 prose prose-md max-w-3xl dark:prose-invert">
|
@ -1,8 +0,0 @@
|
||||
<script lang="ts">
|
||||
// @ts-expect-error - Markdown files are not recognized by Vite
|
||||
import { html } from '/CHANGELOG.md';
|
||||
</script>
|
||||
|
||||
<div class="md:py-8 prose prose-md max-w-3xl dark:prose-invert">
|
||||
{@html html}
|
||||
</div>
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,3 +1,7 @@
|
||||
<script context="module">
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Contact | {import.meta.env.VITE_BRANDING}</title>
|
||||
</svelte:head>
|
||||
@ -5,12 +9,12 @@
|
||||
<div class="md:py-8 prose max-w-3xl dark:prose-invert">
|
||||
<h1>Contact</h1>
|
||||
|
||||
Hi 🙋 I'm
|
||||
Hi! I'm
|
||||
<a href="https://mcndt.dev">mcndt</a>
|
||||
and I build the Obsidian QuickShare plugin and operate Noteshare.space. There are several ways to get
|
||||
in touch with me.
|
||||
|
||||
<h2>🐛 Bugs and feature requests</h2>
|
||||
<h2>Bugs and feature requests</h2>
|
||||
<p>
|
||||
The preferred way to report bugs or request new features for the web app or the Obsidian plugin
|
||||
is via the
|
||||
@ -19,7 +23,7 @@
|
||||
>.
|
||||
</p>
|
||||
|
||||
<h2>❓ Inquiries about Noteshare.space</h2>
|
||||
<h2>Inquiries about Noteshare.space</h2>
|
||||
<p>
|
||||
For questions and inquiries about Noteshare.space (the "official" note hosting service for the
|
||||
Obsidian QuickShare plugin), please E-mail me at <a href="mailto:contact@noteshare.space"
|
||||
@ -27,14 +31,14 @@
|
||||
>.
|
||||
</p>
|
||||
|
||||
<h2>💬 Community</h2>
|
||||
<h2>Community</h2>
|
||||
<p>
|
||||
If you want a more interactive way to discuss bugs or features, or just want to chat about how
|
||||
you use QuickShare and Noteshare.space, you can join the
|
||||
<a href="https://discord.gg/y3HqyGeABK">Discord server</a>.
|
||||
</p>
|
||||
|
||||
<h2>👋 Get to know me!</h2>
|
||||
<h2>Get to know me!</h2>
|
||||
<p>
|
||||
I’m a computer science engineer with interest in a wide range of topics, including productivity,
|
||||
PKM, artificial intelligence, product development and game design.
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,25 +0,0 @@
|
||||
# Operational Expenses and Funding 💰
|
||||
|
||||
## Operational Expenses
|
||||
|
||||
I architected the serverside code to be as light and portable as possible. Currently, it runs on a VPS I rent for about **7$/month**. This is currently the only operational expense for Noteshare.space.
|
||||
|
||||
The storage requirements for hosting encrypted text data are incredible small; at the time of writing (28 September, 2022), Noteshare is storing **1,000 notes in a little over 7MB**.
|
||||
|
||||
### Future expenses: hosting file attachments
|
||||
|
||||
For those who have read [the roadmap](/roadmap), I am working on supporting embedded images and file attachments in a future update. As you can imagine, this greatly increases the storage requirements, as just one attachement has a size in the order of megabytes.
|
||||
|
||||
As I develop this feature, I am still deciding on how I will fund this: community donors, tiered access, ... If you have any suggestions, feel free to reach out to me via [email](mailto:contact@noteshare.space) or the [community Discord](https://discord.gg/y3HqyGeABK).
|
||||
|
||||
## Funding
|
||||
|
||||
I already rented this VPS for other purposes, so I haven no problem paying out of pocket for hosting and you can expect the service to be available
|
||||
|
||||
If you wish to financially support me and the development of this plugin and service, you can send donations via my [_Buy me a coffee_-profile](https://www.buymeacoffee.com/mcndt).
|
||||
|
||||
## Self-hosting
|
||||
|
||||
If you are still worried about relying on me for keeping Noteshare up and running, it is an option to host your own instance of Noteshare. You can find the source code, as well as a `docker-compose.yml` file to get you up and running on the [Noteshare GitHub repo](https://github.com/mcndt/Noteshare.space).
|
||||
|
||||
_Last updated: 2022-09-28_
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,31 +1,31 @@
|
||||
<script context="module">
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Tile from '$lib/components/index/tile.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{import.meta.env.VITE_BRANDING} — Securely share your Obsidian notes with one click.</title
|
||||
>
|
||||
<title>{import.meta.env.VITE_BRANDING} | Create share links for Obsidian in one click</title>
|
||||
</svelte:head>
|
||||
|
||||
<article class="mx-auto max-w-4xl text-zinc-900 dark:text-zinc-100">
|
||||
<div class="space-y-6 pt-20 pb-16 md:pb-24 px-4 md:px-0">
|
||||
<div class="space-y-6 pt-20 pb-24 px-4 md:px-0">
|
||||
<h1 id="title" class="font-extrabold text-4xl md:text-5xl text-center">
|
||||
Securely share your <span class="text-[#705dcf]">Obsidian</span> notes with one click.
|
||||
</h1>
|
||||
<p id="tagline" class="prose-xl md:prose-2xl text-center text-zinc-700 dark:text-zinc-300">
|
||||
Zero configuration. End-to-end encrypted. <br />No account needed.
|
||||
</p>
|
||||
<div id="install-button" class="text-center pt-8">
|
||||
<p id="install-button" class="text-center pt-2">
|
||||
<a href="/install">
|
||||
<button
|
||||
class="py-1.5 px-4 rounded-lg border-2 border-[#705dcf] text-[#705dcf] font-semibold hover:bg-[#705dcf] hover:text-white transition-colors"
|
||||
>Install plugin</button
|
||||
>
|
||||
</a>
|
||||
<p class="mt-2.5 italic text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Now on the Obsidian <br /> community plugin marketplace!
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr class="border-zinc-200 dark:border-zinc-700 transition-colors" />
|
@ -1,3 +1,7 @@
|
||||
<script context="module">
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Get the plugin | {import.meta.env.VITE_BRANDING}</title>
|
||||
<script src="https://tarptaeya.github.io/repo-card/repo-card.js"></script>
|
||||
@ -6,27 +10,10 @@
|
||||
<div class="md:py-8 prose max-w-3xl dark:prose-invert">
|
||||
<h1>Installing the Obsidian Plugin</h1>
|
||||
|
||||
<h2>📦 Install from the community plugin marketplace</h2>
|
||||
The plugin and Noteshare.space are currently in beta. Therefore, the plugin is not yet available
|
||||
through the Obsidian community plugins marketplace.
|
||||
|
||||
<p>
|
||||
If you have Obsidian installed on this device, you can <a
|
||||
href="obsidian://show-plugin?id=obsidian-quickshare"
|
||||
>
|
||||
click here</a
|
||||
>
|
||||
to open Obsidian and install the plugin. Alternatively, you can install the plugin manually by following
|
||||
the instructions below.
|
||||
</p>
|
||||
<ol>
|
||||
<li>Open the Obsidian settings menu.</li>
|
||||
<li>In the sidebar, click "Community plugins".</li>
|
||||
<li>Click the "Browse" button on the right.</li>
|
||||
<li>Type "QuickShare" in the search bar.</li>
|
||||
<li>Click "Install".</li>
|
||||
<li><strong>!! Don't forget to activate the plugin after installation !!</strong></li>
|
||||
</ol>
|
||||
|
||||
<h2>👷 Beta testing with BRAT</h2>
|
||||
<h2>Beta testing with BRAT</h2>
|
||||
|
||||
<p>
|
||||
To beta test, you can install the plugin using BRAT and the GitHub URL (<a
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,23 +1,51 @@
|
||||
<script context="module" , lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async ({ props }) => {
|
||||
const note: EncryptedNote = props.note;
|
||||
const maxage = Math.floor((note.expire_time.valueOf() - note.insert_time.valueOf()) / 1000);
|
||||
return {
|
||||
status: 200,
|
||||
cache: {
|
||||
maxage: maxage,
|
||||
private: false
|
||||
},
|
||||
props: { note }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { decrypt } from '$lib/crypto/decrypt';
|
||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||
import LogoMarkdown from 'svelte-icons/io/IoLogoMarkdown.svelte';
|
||||
import IconEncrypted from 'svelte-icons/md/MdLockOutline.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import type { EncryptedNote } from '$lib/model/EncryptedNote';
|
||||
import { browser } from '$app/env';
|
||||
import RawRenderer from '$lib/components/RawRenderer.svelte';
|
||||
import LogoDocument from 'svelte-icons/md/MdUndo.svelte';
|
||||
import Dismissable from '$lib/components/Dismissable.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let { note } = data;
|
||||
|
||||
// Auto-loaded from [id].ts endpoint
|
||||
export let note: EncryptedNote;
|
||||
let plaintext: string;
|
||||
let timeString: string;
|
||||
let decryptFailed = false;
|
||||
let showRaw = false;
|
||||
let fileTitle: string | undefined;
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const key = location.hash.slice(1);
|
||||
decrypt({ ...note, key }, note.crypto_version)
|
||||
.then((value) => (plaintext = value))
|
||||
.catch(() => (decryptFailed = true));
|
||||
}
|
||||
});
|
||||
|
||||
$: if (note.insert_time) {
|
||||
const diff_ms = new Date().valueOf() - new Date(note.insert_time).valueOf();
|
||||
timeString = msToString(diff_ms);
|
||||
}
|
||||
|
||||
function toggleRaw() {
|
||||
showRaw = !showRaw;
|
||||
@ -39,33 +67,6 @@
|
||||
const months = days / 30.42;
|
||||
return `${Math.floor(months)} month${months >= 2 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
function parsePayload(payload: string): { body: string; title?: string } {
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
return { body: parsed?.body, title: parsed?.title };
|
||||
} catch (e) {
|
||||
return { body: payload, title: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (browser && note) {
|
||||
const key = location.hash.slice(1);
|
||||
decrypt({ ...note, key }, note.crypto_version)
|
||||
.then((value) => {
|
||||
const { body, title } = parsePayload(value);
|
||||
plaintext = body;
|
||||
fileTitle = title;
|
||||
})
|
||||
.catch(() => (decryptFailed = true));
|
||||
}
|
||||
});
|
||||
|
||||
$: if (note?.insert_time) {
|
||||
const diff_ms = new Date().valueOf() - new Date(note.insert_time).valueOf();
|
||||
timeString = msToString(diff_ms);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@ -77,8 +78,6 @@
|
||||
|
||||
{#if plaintext}
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<Dismissable />
|
||||
|
||||
<p
|
||||
class="mb-4 text-sm flex gap-2 flex-col md:gap-0 md:flex-row justify-between text-zinc-500 dark:text-zinc-400"
|
||||
>
|
||||
@ -102,7 +101,7 @@
|
||||
{#if showRaw}
|
||||
<RawRenderer>{plaintext}</RawRenderer>
|
||||
{:else}
|
||||
<MarkdownRenderer {plaintext} {fileTitle} />
|
||||
<MarkdownRenderer {plaintext} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
@ -1,10 +1,8 @@
|
||||
import type { EncryptedNote } from '$lib/model/EncryptedNote';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ request, params, setHeaders, getClientAddress }) => {
|
||||
const ip = (request.headers.get('x-forwarded-for') || getClientAddress()) as string;
|
||||
export const get: RequestHandler = async ({ request, clientAddress, params }) => {
|
||||
const ip = (request.headers.get('x-forwarded-for') || clientAddress) as string;
|
||||
const url = `${import.meta.env.VITE_SERVER_INTERNAL}/api/note/${params.id}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
@ -18,18 +16,27 @@ export const load: PageServerLoad = async ({ request, params, setHeaders, getCli
|
||||
note.insert_time = new Date(note.insert_time as unknown as string);
|
||||
note.expire_time = new Date(note.expire_time as unknown as string);
|
||||
const maxage = Math.floor((note.expire_time.valueOf() - note.insert_time.valueOf()) / 1000);
|
||||
|
||||
setHeaders({
|
||||
maxage: `${maxage}`,
|
||||
'Cache-Control': `max-age=${maxage}, public`
|
||||
});
|
||||
return { note };
|
||||
return {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Cache-Control': `public, max-age=${maxage}`
|
||||
},
|
||||
cache: {
|
||||
maxage: maxage,
|
||||
private: false
|
||||
},
|
||||
body: { note }
|
||||
};
|
||||
} catch {
|
||||
throw error(500, response.statusText);
|
||||
return {
|
||||
status: 500,
|
||||
error: response.statusText
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// get the response body (the reason why the request failed)
|
||||
const body = await response.text();
|
||||
throw error(response.status, body);
|
||||
return {
|
||||
status: response.status,
|
||||
error: response.statusText
|
||||
};
|
||||
}
|
||||
};
|
34
webapp/src/routes/note/[note_id]/embeds/[id].ts
Normal file
34
webapp/src/routes/note/[note_id]/embeds/[id].ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { EncryptedEmbed } from '$lib/model/EncryptedEmbed';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async ({ request, clientAddress, params }) => {
|
||||
const ip = (request.headers.get('x-forwarded-for') || clientAddress) as string;
|
||||
const url = `${import.meta.env.VITE_SERVER_INTERNAL}/api/note/${params.note_id}/embeds/${
|
||||
params.id
|
||||
}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'x-forwarded-for': ip
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
try {
|
||||
const embed: EncryptedEmbed = await response.json();
|
||||
return {
|
||||
status: response.status,
|
||||
body: embed
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: 500,
|
||||
error: response.statusText
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: response.status,
|
||||
error: response.statusText
|
||||
};
|
||||
}
|
||||
};
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,41 +0,0 @@
|
||||
<svelte:head>
|
||||
<title>Roadmap | {import.meta.env.VITE_BRANDING}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="md:py-8 prose max-w-3xl dark:prose-invert">
|
||||
<h1>Roadmap</h1>
|
||||
|
||||
<p>
|
||||
Here you can find the current roadmap for the plugin. If you have any suggestions for the web
|
||||
app or the Obsidian plugin is via the <a
|
||||
href="https://github.com/mcndt/obsidian-quickshare/issues"
|
||||
>
|
||||
GitHub issues page
|
||||
</a>. For a more detailed overview of open issues and bugs on this open source project, check
|
||||
out the <a href="https://github.com/users/mcndt/projects/1/views/5">GitHub project page</a>.
|
||||
</p>
|
||||
|
||||
<div class="md:grid md:grid-cols-3 gap-x-2 w-full">
|
||||
<div>
|
||||
<h2>⚙️ In progress</h2>
|
||||
<ul>
|
||||
<li>Adding embedded images to shared note.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>🗓️ Planned</h2>
|
||||
<ul>
|
||||
<li>Deleting previously shared notes.</li>
|
||||
<li>Manually setting an expire time.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>💭 Potential features</h2>
|
||||
<ul>
|
||||
<li>Rendering DataView codeblocks before sharing.</li>
|
||||
<li>Rendering Excalidraw embeds before sharing.</li>
|
||||
<li>Rendering Mermaid diagrams in the web app.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { readMd } from './util';
|
||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||
|
||||
const testCases = [
|
||||
@ -44,11 +45,9 @@ describe.each(testCases)('Rendering callouts', async (testCase) => {
|
||||
expect(titleEl).toHaveClass('callout-title');
|
||||
});
|
||||
|
||||
// TODO: this test is broken. Need to fix it.
|
||||
it.skip('Renders callout content correctly ', async () => {
|
||||
it('Renders callout content correctly ', async () => {
|
||||
render(MarkdownRenderer, { plaintext: testCase.markdown });
|
||||
const contentEl = await screen.findByText(testCase.content);
|
||||
// const contentEl = await screen.findByText(testCase.content);
|
||||
expect(contentEl).toBeInTheDocument();
|
||||
expect(contentEl.parentElement).toHaveClass('callout-content');
|
||||
});
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 8.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
@ -1,6 +1,8 @@
|
||||
// import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
import { plugin as markdown } from 'vite-plugin-markdown';
|
||||
import { searchForWorkspaceRoot } from 'vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
@ -12,7 +14,24 @@ const config = {
|
||||
})
|
||||
],
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
adapter: adapter(),
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
include: ['highlight.js', 'highlight.js/lib/core']
|
||||
},
|
||||
plugins: [markdown({ mode: ['html', 'toc'] })],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'happy-dom',
|
||||
setupFiles: ['setupTest.js']
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
// Allow serving CHANGELOG.md file
|
||||
allow: [searchForWorkspaceRoot(process.cwd()), '/CHANGELOG.md']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { plugin as markdown } from 'vite-plugin-markdown';
|
||||
import { searchForWorkspaceRoot } from 'vite';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit(), markdown({ mode: ['html', 'toc'] })],
|
||||
optimizeDeps: {
|
||||
include: ['highlight.js', 'highlight.js/lib/core']
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'happy-dom',
|
||||
setupFiles: ['setupTest.js']
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
// Allow serving CHANGELOG.md file
|
||||
allow: [searchForWorkspaceRoot(process.cwd()), '/CHANGELOG.md']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
3
webapp/vitest.config.ts
Normal file
3
webapp/vitest.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { extractFromSvelteConfig } from 'vitest-svelte-kit';
|
||||
|
||||
export default extractFromSvelteConfig();
|
Loading…
Reference in New Issue
Block a user