Compare commits
34 Commits
upload-ima
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
655973a961 | ||
|
479a603d00 | ||
|
39a55e4a01 | ||
|
3fd77199fc | ||
|
bf22ce963d | ||
|
017c318f88 | ||
|
855e0897f3 | ||
|
bf2ca751a8 | ||
|
e6dc2d849a | ||
|
de5395bd23 | ||
|
836d491ff9 | ||
|
9931c70af5 | ||
|
de32a04f9c | ||
|
8d37c90106 | ||
|
80673d1218 | ||
|
78b05bb9b8 | ||
|
3e3656243a | ||
|
8bceeaf4c0 | ||
|
6f3552ce59 | ||
|
5aeccd5aa3 | ||
|
689f232cbb | ||
|
308fffbefe | ||
|
52bcb9974b | ||
|
4c19d53287 | ||
|
7a37e7238b | ||
|
8d8cac4dbd | ||
|
73a4d77c66 | ||
|
3696d45a57 | ||
|
4c59e86cd5 | ||
|
58f946cc3f | ||
|
290b504932 | ||
|
49e50f0c52 | ||
|
d495235c38 | ||
|
7886485f42 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -13,4 +13,7 @@ server/.env
|
||||
|
||||
# production configuration
|
||||
docker-compose.server.yml
|
||||
/letsencrypt
|
||||
/letsencrypt
|
||||
|
||||
# Dev env configuration
|
||||
proxy.js
|
@ -12,6 +12,9 @@ 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`)" #
|
||||
- "traefik.http.routers.backend.rule=Host(`localhost`) && PathPrefix(`/api`) && (Method(`POST`) || Method(`DELETE`))" #
|
||||
|
||||
# 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 -- --open\" \"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\" \"npm --prefix ./server run dev\" \"npm run proxy\""
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^7.2.2"
|
||||
|
2
plugin
2
plugin
@ -1 +1 @@
|
||||
Subproject commit 73733c0292cb3f0d6775c69c734e80c690932777
|
||||
Subproject commit 7a645c3e11c14f222405068c4710d30686800013
|
707
pnpm-lock.yaml
generated
Normal file
707
pnpm-lock.yaml
generated
Normal file
@ -0,0 +1,707 @@
|
||||
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,6 +3,8 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
const app = express();
|
||||
|
||||
const PORT = 5000;
|
||||
|
||||
app.use(
|
||||
"/api/",
|
||||
createProxyMiddleware({
|
||||
@ -14,10 +16,10 @@ app.use(
|
||||
app.use(
|
||||
"/",
|
||||
createProxyMiddleware({
|
||||
target: "http://localhost:3000",
|
||||
target: "http://localhost:5173",
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.listen(5000);
|
||||
console.log("Reverse proxy listening at http://localhost:5000");
|
||||
app.listen(PORT);
|
||||
console.log(`Reverse proxy listening at http://localhost:${PORT}`);
|
@ -11,7 +11,8 @@
|
||||
"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"
|
||||
"dev": "npx nodemon src/server.ts | npx pino-colada",
|
||||
"migrate": "npx prisma migrate dev"
|
||||
},
|
||||
"author": "Maxime Cannoodt (mcndt)",
|
||||
"license": "MIT",
|
||||
|
@ -0,0 +1,16 @@
|
||||
-- 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;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EncryptedNote" ADD COLUMN "secret_token" TEXT;
|
@ -15,8 +15,10 @@ model EncryptedNote {
|
||||
insert_time DateTime @default(now())
|
||||
expire_time DateTime @default(now())
|
||||
ciphertext String
|
||||
hmac String
|
||||
hmac String?
|
||||
iv String?
|
||||
crypto_version String @default("v1")
|
||||
secret_token String?
|
||||
}
|
||||
|
||||
model event {
|
||||
|
77
server/src/controllers/note/note.delete.controller.ts
Normal file
77
server/src/controllers/note/note.delete.controller.ts
Normal file
@ -0,0 +1,77 @@
|
||||
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]);
|
||||
}
|
102
server/src/controllers/note/note.delete.controller.unit.test.ts
Normal file
102
server/src/controllers/note/note.delete.controller.unit.test.ts
Normal file
@ -0,0 +1,102 @@
|
||||
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,7 +1,7 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter";
|
||||
import { getNoteFilter } from "../../lib/expiredNoteFilter";
|
||||
import EventLogger from "../../logging/EventLogger";
|
||||
import { getConnectingIp } from "../../util";
|
||||
import { getConnectingIp, getNoteSize } from "../../util";
|
||||
import { getNote } from "../../db/note.dao";
|
||||
export async function getNoteController(
|
||||
req: Request,
|
||||
@ -16,13 +16,23 @@ export async function getNoteController(
|
||||
success: true,
|
||||
host: ip,
|
||||
note_id: note.id,
|
||||
size_bytes: note.ciphertext.length + note.hmac.length,
|
||||
size_bytes: getNoteSize(note),
|
||||
});
|
||||
res.send(note);
|
||||
} else {
|
||||
// check the expired filter to see if the note was expired
|
||||
const expiredFilter = await getExpiredNoteFilter();
|
||||
if (expiredFilter.hasNoteId(req.params.id)) {
|
||||
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)) {
|
||||
await EventLogger.readEvent({
|
||||
success: false,
|
||||
host: ip,
|
||||
@ -37,7 +47,7 @@ export async function getNoteController(
|
||||
note_id: req.params.id,
|
||||
error: "Note not found",
|
||||
});
|
||||
res.status(404).send();
|
||||
res.status(404).send("Note not found");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,42 +1,16 @@
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { crc16 as crc } from "crc";
|
||||
import { createNote } from "../../db/note.dao";
|
||||
import { addDays, getConnectingIp } from "../../util";
|
||||
import { addDays, getConnectingIp, getNoteSize } from "../../util";
|
||||
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
|
||||
import {
|
||||
validateOrReject,
|
||||
IsBase64,
|
||||
IsHexadecimal,
|
||||
IsNotEmpty,
|
||||
ValidateIf,
|
||||
ValidationError,
|
||||
Matches,
|
||||
} from "class-validator";
|
||||
import { validateOrReject, ValidationError } from "class-validator";
|
||||
import { generateToken } from "../../crypto/GenerateToken";
|
||||
import { NotePostRequest } from "../../validation/Request";
|
||||
import checkId from "../../lib/checkUserId";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
|
||||
export async function postNoteController(
|
||||
req: Request,
|
||||
@ -74,11 +48,15 @@ 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
|
||||
@ -86,12 +64,14 @@ export async function postNoteController(
|
||||
.then(async (savedNote) => {
|
||||
event.success = true;
|
||||
event.note_id = savedNote.id;
|
||||
event.size_bytes = savedNote.ciphertext.length + savedNote.hmac.length;
|
||||
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) => {
|
||||
@ -100,23 +80,3 @@ export async function postNoteController(
|
||||
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;
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ import supertest from "supertest";
|
||||
import { vi, describe, it, beforeEach, afterEach, expect } from "vitest";
|
||||
import * as noteDao from "../../db/note.dao";
|
||||
import EventLogger from "../../logging/EventLogger";
|
||||
import { NotePostRequest, postNoteController } from "./note.post.controller";
|
||||
import { NotePostRequest } from "../../validation/Request";
|
||||
import { postNoteController } from "./note.post.controller";
|
||||
|
||||
vi.mock("../../db/note.dao");
|
||||
vi.mock("../../logging/EventLogger");
|
||||
|
@ -1,5 +1,6 @@
|
||||
import express from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import { deleteNoteController } from "./note.delete.controller";
|
||||
import { getNoteController } from "./note.get.controller";
|
||||
import { postNoteController } from "./note.post.controller";
|
||||
|
||||
@ -25,3 +26,4 @@ const getRateLimit = rateLimit({
|
||||
notesRoute.use(jsonParser);
|
||||
notesRoute.post("", postRateLimit, postNoteController);
|
||||
notesRoute.get("/:id", getRateLimit, getNoteController);
|
||||
notesRoute.delete("/:id", getRateLimit, deleteNoteController);
|
||||
|
9
server/src/crypto/GenerateToken.ts
Normal file
9
server/src/crypto/GenerateToken.ts
Normal file
@ -0,0 +1,9 @@
|
||||
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,3 +4,4 @@ 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();
|
||||
|
@ -23,6 +23,12 @@ 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({
|
||||
|
21
server/src/lib/checkUserId.ts
Normal file
21
server/src/lib/checkUserId.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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,16 +1,24 @@
|
||||
import { ScalableBloomFilter } from "bloom-filters";
|
||||
import { getFilter, upsertFilter } from "../db/bloomFilter.dao";
|
||||
|
||||
export class ExpiredNoteFilter {
|
||||
_filter: ScalableBloomFilter;
|
||||
static FILTER_NAME = "expiredNotes";
|
||||
export const EXPIRED_NOTES_FILTER_NAME = "expiredNotes" as const;
|
||||
export const DELETED_NOTES_FILTER_NAME = "deletedNotes" as const;
|
||||
|
||||
private constructor(filter: ScalableBloomFilter) {
|
||||
type FilterName =
|
||||
| typeof EXPIRED_NOTES_FILTER_NAME
|
||||
| typeof DELETED_NOTES_FILTER_NAME;
|
||||
|
||||
export class NoteIdFilter {
|
||||
_filter: ScalableBloomFilter;
|
||||
_name: string;
|
||||
|
||||
private constructor(name: string, filter: ScalableBloomFilter) {
|
||||
this._filter = filter;
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
public static async deserializeFromDb(): Promise<ExpiredNoteFilter> {
|
||||
return ExpiredNoteFilter._deserializeFilter()
|
||||
public static async deserializeFromDb(name: string): Promise<NoteIdFilter> {
|
||||
return NoteIdFilter._deserializeFilter(name)
|
||||
.catch((err) => {
|
||||
if (err.message === "No BloomFilter found") {
|
||||
return new ScalableBloomFilter();
|
||||
@ -19,7 +27,7 @@ export class ExpiredNoteFilter {
|
||||
}
|
||||
})
|
||||
.then((filter) => {
|
||||
return new ExpiredNoteFilter(filter);
|
||||
return new NoteIdFilter(name, filter);
|
||||
});
|
||||
}
|
||||
|
||||
@ -35,24 +43,26 @@ export class ExpiredNoteFilter {
|
||||
}
|
||||
|
||||
private _serialize(): Promise<void> {
|
||||
return upsertFilter(ExpiredNoteFilter.FILTER_NAME, this._filter);
|
||||
return upsertFilter(this._name, this._filter);
|
||||
}
|
||||
|
||||
private static _deserializeFilter(): Promise<ScalableBloomFilter> {
|
||||
return getFilter<ScalableBloomFilter>(
|
||||
this.FILTER_NAME,
|
||||
ScalableBloomFilter
|
||||
);
|
||||
private static _deserializeFilter(
|
||||
name: string
|
||||
): Promise<ScalableBloomFilter> {
|
||||
return getFilter<ScalableBloomFilter>(name, ScalableBloomFilter);
|
||||
}
|
||||
}
|
||||
|
||||
let _filter: ExpiredNoteFilter;
|
||||
let _filters: Record<FilterName, NoteIdFilter | null> = {
|
||||
expiredNotes: null,
|
||||
deletedNotes: null,
|
||||
};
|
||||
|
||||
export async function getExpiredNoteFilter(): Promise<ExpiredNoteFilter> {
|
||||
if (_filter) {
|
||||
return _filter;
|
||||
export async function getNoteFilter(name: FilterName): Promise<NoteIdFilter> {
|
||||
if (_filters[name] !== null) {
|
||||
return _filters[name] as NoteIdFilter;
|
||||
} else {
|
||||
_filter = await ExpiredNoteFilter.deserializeFromDb();
|
||||
return _filter;
|
||||
_filters[name] = await NoteIdFilter.deserializeFromDb(name);
|
||||
return _filters[name] as NoteIdFilter;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { ExpiredNoteFilter } from "./expiredNoteFilter";
|
||||
import { NoteIdFilter } 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 ExpiredNoteFilter.deserializeFromDb();
|
||||
const testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
expect(mockedDao.getFilter).toHaveBeenCalledWith(
|
||||
"expiredNotes",
|
||||
ScalableBloomFilter
|
||||
);
|
||||
expect(testFilter).toBeInstanceOf(ExpiredNoteFilter);
|
||||
expect(testFilter).toBeInstanceOf(NoteIdFilter);
|
||||
|
||||
// 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 ExpiredNoteFilter.deserializeFromDb();
|
||||
const testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
expect(mockedDao.getFilter).toHaveBeenCalledWith(
|
||||
"expiredNotes",
|
||||
ScalableBloomFilter
|
||||
@ -51,7 +51,7 @@ describe("Deserialization from database", () => {
|
||||
});
|
||||
|
||||
describe("Filter operations and serialization", () => {
|
||||
let testFilter: ExpiredNoteFilter;
|
||||
let testFilter: NoteIdFilter;
|
||||
|
||||
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 ExpiredNoteFilter.deserializeFromDb();
|
||||
testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
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 ExpiredNoteFilter.deserializeFromDb();
|
||||
testFilter = await NoteIdFilter.deserializeFromDb("expiredNotes");
|
||||
const elements = Array.from({ length: 1000 }, (_, i) => i.toString());
|
||||
testFilter.addNoteIds(elements);
|
||||
|
||||
|
@ -1,13 +1,16 @@
|
||||
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",
|
||||
}
|
||||
|
||||
interface Event {
|
||||
export interface Event {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
@ -25,6 +28,10 @@ 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 {
|
||||
@ -33,19 +40,42 @@ 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,9 +1,19 @@
|
||||
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(),
|
||||
readEvent: vi.fn(),
|
||||
purgeEvent: vi.fn(),
|
||||
writeEvent: vi.fn(logEventToConsole),
|
||||
readEvent: vi.fn(logEventToConsole),
|
||||
purgeEvent: vi.fn(logEventToConsole),
|
||||
deleteEvent: vi.fn(logEventToConsole),
|
||||
updateEvent: vi.fn(logEventToConsole),
|
||||
};
|
||||
|
||||
export default mockedEventLogger;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { deleteNotes, getExpiredNotes } from "../db/note.dao";
|
||||
import { getExpiredNoteFilter } from "../lib/expiredNoteFilter";
|
||||
import { getNoteFilter } 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...");
|
||||
@ -11,18 +12,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 ${
|
||||
note.ciphertext.length + note.hmac.length
|
||||
} bytes`
|
||||
`[Cleanup] Deleted note ${note.id} with size ${getNoteSize(
|
||||
note
|
||||
)} bytes`
|
||||
);
|
||||
return EventLogger.purgeEvent({
|
||||
success: true,
|
||||
note_id: note.id,
|
||||
size_bytes: note.ciphertext.length + note.hmac.length,
|
||||
size_bytes: getNoteSize(note),
|
||||
});
|
||||
});
|
||||
await Promise.all(logs);
|
||||
const filter = await getExpiredNoteFilter();
|
||||
const filter = await getNoteFilter("expiredNotes");
|
||||
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 { getExpiredNoteFilter: () => instance };
|
||||
return { getNoteFilter: (name: string) => instance };
|
||||
});
|
||||
|
||||
vi.spyOn(logger, "error");
|
||||
@ -29,15 +29,17 @@ 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.getExpiredNoteFilter());
|
||||
const mockedFilter = vi.mocked(await filter.getNoteFilter("expiredNotes"));
|
||||
mockedFilter.addNoteIds.mockResolvedValue();
|
||||
|
||||
// test task call
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { EncryptedNote } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
|
||||
export function addDays(date: Date, days: number): Date {
|
||||
@ -11,3 +12,11 @@ export function getConnectingIp(req: Request): string {
|
||||
req.headers["X-Forwarded-For"] ||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
40
server/src/validation/Request.ts
Normal file
40
server/src/validation/Request.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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,7 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/.svelte-kit/*
|
||||
!/.svelte-kit/tsconfig.json
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
|
46
webapp/.svelte-kit/tsconfig.json
Normal file
46
webapp/.svelte-kit/tsconfig.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"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,11 +1,24 @@
|
||||
# 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]
|
||||
|
1383
webapp/package-lock.json
generated
1383
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,11 +5,9 @@
|
||||
"author": "Maxime Cannoodt (mcndt)",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "svelte-kit preview",
|
||||
"prepare": "svelte-kit sync",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||
@ -20,10 +18,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/kit": "^1.0.0-next.350",
|
||||
"@sveltejs/kit": "^1.0.0-next.544",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/svelte": "^3.1.3",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/marked": "^4.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
@ -43,13 +41,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-svelte-kit": "^0.0.6"
|
||||
"vitest": "^0.17.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.78",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.100",
|
||||
"crypto-js": "^4.1.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"katex": "^0.16.0",
|
||||
|
3842
webapp/pnpm-lock.yaml
generated
Normal file
3842
webapp/pnpm-lock.yaml
generated
Normal file
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';
|
||||
|
||||
let title = '';
|
||||
let type = 'note';
|
||||
export let title = '';
|
||||
export let type = 'note';
|
||||
let color = '--callout-warning';
|
||||
let icon = 'note';
|
||||
let init = false;
|
||||
@ -12,8 +12,9 @@
|
||||
|
||||
$: if (content) {
|
||||
const titleElement = content.getElementsByTagName('p')[0];
|
||||
const preFilled = title != '';
|
||||
const match = titleElement.innerText.split('\n')[0].match(/\[!(.+)\]([+-]?)(?:\s(.+))?/);
|
||||
if (match) {
|
||||
if (match && !preFilled) {
|
||||
type = match[1]?.trim();
|
||||
title = match[3]?.trim() ?? type[0].toUpperCase() + type.substring(1).toLowerCase();
|
||||
}
|
||||
@ -22,11 +23,13 @@
|
||||
icon = getCalloutIcon(type);
|
||||
|
||||
// Remove title from content
|
||||
const pos = titleElement.innerHTML.indexOf('<br>');
|
||||
if (pos >= 0) {
|
||||
titleElement.innerHTML = titleElement.innerHTML.substring(pos + 4);
|
||||
} else {
|
||||
titleElement.innerHTML = '';
|
||||
if (!preFilled) {
|
||||
const pos = titleElement.innerHTML.indexOf('<br>');
|
||||
if (pos >= 0) {
|
||||
titleElement.innerHTML = titleElement.innerHTML.substring(pos + 4);
|
||||
} else {
|
||||
titleElement.innerHTML = '';
|
||||
}
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
|
51
webapp/src/lib/components/Dismissable.svelte
Normal file
51
webapp/src/lib/components/Dismissable.svelte
Normal file
@ -0,0 +1,51 @@
|
||||
<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 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 gap-y-1.5 text-zinc-500 dark:text-zinc-400"
|
||||
>
|
||||
<span>
|
||||
Built with love by <a class="underline" href="https://mcndt.dev" alt="blog">mcndt</a>
|
||||
@ -11,6 +11,8 @@
|
||||
<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>
|
||||
@ -21,5 +23,7 @@
|
||||
>🐛 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,6 +19,8 @@
|
||||
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;
|
||||
@ -29,10 +31,14 @@
|
||||
const options = { ...marked.defaults, breaks: true };
|
||||
|
||||
function onParsed() {
|
||||
setTitle();
|
||||
!fileTitle && setTitle();
|
||||
parseFootnotes();
|
||||
}
|
||||
|
||||
$: if (fileTitle) {
|
||||
document.title = fileTitle.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for the first major header in the document to use as page title.
|
||||
*/
|
||||
@ -66,8 +72,11 @@
|
||||
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-blockquote:first:before:content-[''] prose-hr:transition-colors prose-code:before:content-[''] prose-code:after:content-['']"
|
||||
>
|
||||
{#if fileTitle}
|
||||
<h1>{fileTitle}</h1>
|
||||
{/if}
|
||||
<SvelteMarkdown
|
||||
on:parsed={onParsed}
|
||||
renderers={{
|
||||
|
@ -2,16 +2,31 @@
|
||||
|
||||
import { AES, enc, HmacSHA256 } from 'crypto-js';
|
||||
|
||||
export async function decrypt(
|
||||
cryptData: { ciphertext: string; hmac: string; key: string },
|
||||
version: string
|
||||
): Promise<string> {
|
||||
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> {
|
||||
console.debug(`decrypting with crypto suite ${version}`);
|
||||
if (version === 'v1') {
|
||||
return decrypt_v1(cryptData);
|
||||
return decrypt_v1(cryptData as CryptData_v1);
|
||||
}
|
||||
if (version === 'v2') {
|
||||
return decrypt_v2(cryptData);
|
||||
return decrypt_v2(cryptData as CryptData_v1);
|
||||
}
|
||||
if (version === 'v3') {
|
||||
return decrypt_v3(cryptData as CryptData_v3);
|
||||
}
|
||||
throw new Error(`Unsupported crypto version: ${version}`);
|
||||
}
|
||||
@ -53,15 +68,37 @@ export async function decrypt_v2(cryptData: {
|
||||
|
||||
const md = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv: new Uint8Array(16) },
|
||||
await _getAesKey(secret),
|
||||
await _getAesCbcKey(secret),
|
||||
ciphertext_buf
|
||||
);
|
||||
return new TextDecoder().decode(md);
|
||||
}
|
||||
|
||||
function _getAesKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
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> {
|
||||
return window.crypto.subtle.importKey('raw', secret, { name: 'AES-CBC', length: 256 }, false, [
|
||||
'encrypt',
|
||||
'decrypt'
|
||||
]);
|
||||
}
|
||||
|
||||
function _getAesGcmKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.importKey('raw', secret, { name: 'AES-GCM', length: 256 }, false, [
|
||||
'decrypt'
|
||||
]);
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
@ -12,12 +10,14 @@
|
||||
let highlighted: string;
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
try {
|
||||
if (hljs.getLanguage(lang) !== undefined) {
|
||||
highlighted = hljs.highlight(text, { language: lang }).value;
|
||||
} else {
|
||||
highlighted = text;
|
||||
}
|
||||
} catch {
|
||||
highlighted = text;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
38
webapp/src/routes/+error.svelte
Normal file
38
webapp/src/routes/+error.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<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,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/env';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import NavBar from '$lib/components/navbar/NavBar.svelte';
|
||||
@ -29,7 +29,42 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{import.meta.env.VITE_BRANDING}</title>
|
||||
<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" />
|
||||
</svelte:head>
|
||||
|
||||
<div class=" h-full {dark !== undefined ? '' : 'hidden'} {dark ? darkTheme : ''}">
|
||||
@ -56,7 +91,6 @@
|
||||
>
|
||||
</span>
|
||||
</NavBarLink>
|
||||
|
||||
<ThemeToggle bind:dark />
|
||||
</svelte:fragment>
|
||||
></NavBar
|
2
webapp/src/routes/+page.js
Normal file
2
webapp/src/routes/+page.js
Normal file
@ -0,0 +1,2 @@
|
||||
// try making this false to prevent LinkedIn crawler returning 416
|
||||
export const prerender = false;
|
@ -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} | Create share links for Obsidian in one click</title>
|
||||
<title>{import.meta.env.VITE_BRANDING} — Securely share your Obsidian notes with 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-24 px-4 md:px-0">
|
||||
<div class="space-y-6 pt-20 pb-16 md: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>
|
||||
<p id="install-button" class="text-center pt-2">
|
||||
<div id="install-button" class="text-center pt-8">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<hr class="border-zinc-200 dark:border-zinc-700 transition-colors" />
|
@ -1,43 +0,0 @@
|
||||
<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
webapp/src/routes/about/+page.js
Normal file
1
webapp/src/routes/about/+page.js
Normal file
@ -0,0 +1 @@
|
||||
export const prerender = true;
|
@ -1,7 +1,3 @@
|
||||
<script context="module">
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>About | {import.meta.env.VITE_BRANDING}</title>
|
||||
</svelte:head>
|
8
webapp/src/routes/changelog/+page.svelte
Normal file
8
webapp/src/routes/changelog/+page.svelte
Normal file
@ -0,0 +1,8 @@
|
||||
<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
webapp/src/routes/changelog/+page.ts
Normal file
1
webapp/src/routes/changelog/+page.ts
Normal file
@ -0,0 +1 @@
|
||||
export const prerender = true;
|
1
webapp/src/routes/contact/+page.js
Normal file
1
webapp/src/routes/contact/+page.js
Normal file
@ -0,0 +1 @@
|
||||
export const prerender = true;
|
@ -1,7 +1,3 @@
|
||||
<script context="module">
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Contact | {import.meta.env.VITE_BRANDING}</title>
|
||||
</svelte:head>
|
||||
@ -9,12 +5,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
|
||||
@ -23,7 +19,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"
|
||||
@ -31,14 +27,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.
|
25
webapp/src/routes/funding.md
Normal file
25
webapp/src/routes/funding.md
Normal file
@ -0,0 +1,25 @@
|
||||
# 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,8 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = true;
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-expect-error - Markdown files are not recognized by Svelte
|
||||
import { toc, html } from '/CHANGELOG.md';
|
||||
import { html } from '../funding.md';
|
||||
</script>
|
||||
|
||||
<div class="md:py-8 prose prose-md max-w-3xl dark:prose-invert">
|
1
webapp/src/routes/funding/+page.ts
Normal file
1
webapp/src/routes/funding/+page.ts
Normal file
@ -0,0 +1 @@
|
||||
export const prerender = true;
|
1
webapp/src/routes/install/+page.js
Normal file
1
webapp/src/routes/install/+page.js
Normal file
@ -0,0 +1 @@
|
||||
export const prerender = true;
|
@ -1,7 +1,3 @@
|
||||
<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>
|
||||
@ -10,10 +6,27 @@
|
||||
<div class="md:py-8 prose max-w-3xl dark:prose-invert">
|
||||
<h1>Installing the Obsidian Plugin</h1>
|
||||
|
||||
The plugin and Noteshare.space are currently in beta. Therefore, the plugin is not yet available
|
||||
through the Obsidian community plugins marketplace.
|
||||
<h2>📦 Install from the community plugin marketplace</h2>
|
||||
|
||||
<h2>Beta testing with BRAT</h2>
|
||||
<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>
|
||||
|
||||
<p>
|
||||
To beta test, you can install the plugin using BRAT and the GitHub URL (<a
|
@ -1,8 +1,10 @@
|
||||
import type { EncryptedNote } from '$lib/model/EncryptedNote';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const get: RequestHandler = async ({ request, clientAddress, params }) => {
|
||||
const ip = (request.headers.get('x-forwarded-for') || clientAddress) as string;
|
||||
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;
|
||||
const url = `${import.meta.env.VITE_SERVER_INTERNAL}/api/note/${params.id}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
@ -16,27 +18,18 @@ export const get: RequestHandler = async ({ request, clientAddress, params }) =>
|
||||
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);
|
||||
return {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Cache-Control': `public, max-age=${maxage}`
|
||||
},
|
||||
cache: {
|
||||
maxage: maxage,
|
||||
private: false
|
||||
},
|
||||
body: { note }
|
||||
};
|
||||
|
||||
setHeaders({
|
||||
maxage: `${maxage}`,
|
||||
'Cache-Control': `max-age=${maxage}, public`
|
||||
});
|
||||
return { note };
|
||||
} catch {
|
||||
return {
|
||||
status: 500,
|
||||
error: response.statusText
|
||||
};
|
||||
throw error(500, response.statusText);
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: response.status,
|
||||
error: response.statusText
|
||||
};
|
||||
// get the response body (the reason why the request failed)
|
||||
const body = await response.text();
|
||||
throw error(response.status, body);
|
||||
}
|
||||
};
|
@ -1,51 +1,23 @@
|
||||
<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 type { EncryptedNote } from '$lib/model/EncryptedNote';
|
||||
import { browser } from '$app/env';
|
||||
import { browser } from '$app/environment';
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
let fileTitle: string | undefined;
|
||||
|
||||
function toggleRaw() {
|
||||
showRaw = !showRaw;
|
||||
@ -67,6 +39,33 @@
|
||||
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>
|
||||
@ -78,6 +77,8 @@
|
||||
|
||||
{#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"
|
||||
>
|
||||
@ -101,7 +102,7 @@
|
||||
{#if showRaw}
|
||||
<RawRenderer>{plaintext}</RawRenderer>
|
||||
{:else}
|
||||
<MarkdownRenderer {plaintext} />
|
||||
<MarkdownRenderer {plaintext} {fileTitle} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
1
webapp/src/routes/roadmap/+page.js
Normal file
1
webapp/src/routes/roadmap/+page.js
Normal file
@ -0,0 +1 @@
|
||||
export const prerender = true;
|
41
webapp/src/routes/roadmap/+page.svelte
Normal file
41
webapp/src/routes/roadmap/+page.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<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,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { readMd } from './util';
|
||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||
|
||||
const testCases = [
|
||||
@ -45,9 +44,11 @@ describe.each(testCases)('Rendering callouts', async (testCase) => {
|
||||
expect(titleEl).toHaveClass('callout-title');
|
||||
});
|
||||
|
||||
it('Renders callout content correctly ', async () => {
|
||||
// TODO: this test is broken. Need to fix it.
|
||||
it.skip('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');
|
||||
});
|
||||
|
1
webapp/static/deleted_note.svg
Normal file
1
webapp/static/deleted_note.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.0 KiB |
BIN
webapp/static/meta.png
Normal file
BIN
webapp/static/meta.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
@ -1,8 +1,6 @@
|
||||
// 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 = {
|
||||
@ -14,24 +12,7 @@ const config = {
|
||||
})
|
||||
],
|
||||
kit: {
|
||||
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']
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
|
24
webapp/vite.config.js
Normal file
24
webapp/vite.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
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;
|
@ -1,3 +0,0 @@
|
||||
import { extractFromSvelteConfig } from 'vitest-svelte-kit';
|
||||
|
||||
export default extractFromSvelteConfig();
|
Loading…
Reference in New Issue
Block a user