Compare commits
473 Commits
kurt/githu
...
kurt/serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41798682b0 | ||
|
|
bca9c531a6 | ||
|
|
9e0f1eee60 | ||
|
|
53a6639fd1 | ||
|
|
e9859d85b8 | ||
|
|
a62c5bce03 | ||
|
|
149b2b6360 | ||
|
|
43fc897bf9 | ||
|
|
a909188f15 | ||
|
|
e8da05be8c | ||
|
|
dd0178d554 | ||
|
|
e3efb1a3dd | ||
|
|
cd90c3ce49 | ||
|
|
1ea4f9bdd5 | ||
|
|
e5f2552fc9 | ||
|
|
219f341972 | ||
|
|
e26beda598 | ||
|
|
c402b051e2 | ||
|
|
f7172be68b | ||
|
|
da0a4d6f1c | ||
|
|
0bc759cf9e | ||
|
|
a4cfc37576 | ||
|
|
bc1f12971d | ||
|
|
50cd44cd76 | ||
|
|
55917395b4 | ||
|
|
dc92920481 | ||
|
|
434eb0ef86 | ||
|
|
96ee9c4aa4 | ||
|
|
77014f0d36 | ||
|
|
3df903ffc6 | ||
|
|
68fa10437e | ||
|
|
342953b25f | ||
|
|
421ceee88d | ||
|
|
ae05a79e58 | ||
|
|
549217e953 | ||
|
|
4804c3bfe9 | ||
|
|
6c093e65bf | ||
|
|
4b9a8591ab | ||
|
|
9f769d6a61 | ||
|
|
32d6ef27ad | ||
|
|
c4c195074b | ||
|
|
3f6d919f22 | ||
|
|
aabe682782 | ||
|
|
5efaec4df0 | ||
|
|
6d0c832f6f | ||
|
|
66dc04d98e | ||
|
|
3aa3254e48 | ||
|
|
879f24b08b | ||
|
|
b80ea7f813 | ||
|
|
e9ad7180a7 | ||
|
|
0ce7ce4e76 | ||
|
|
088cfa4f2d | ||
|
|
ab92894a2d | ||
|
|
911744a071 | ||
|
|
d4bfcb4eb8 | ||
|
|
965e5b0f54 | ||
|
|
77799a5870 | ||
|
|
7540c908e7 | ||
|
|
dd152709ff | ||
|
|
2d7fb91f92 | ||
|
|
02463db741 | ||
|
|
38b905e180 | ||
|
|
cc50c984e4 | ||
|
|
9aee4ae725 | ||
|
|
6e45ce96d7 | ||
|
|
892b1d3809 | ||
|
|
83d327ad20 | ||
|
|
c77169cf21 | ||
|
|
0ba4ec4e21 | ||
|
|
06dbc35cf8 | ||
|
|
8170da854d | ||
|
|
d255a78cd1 | ||
|
|
c19658b7f8 | ||
|
|
335dac8677 | ||
|
|
18732e27fc | ||
|
|
64624b9c3e | ||
|
|
a89f2e7992 | ||
|
|
6d7f6fb4bf | ||
|
|
b621d78eb4 | ||
|
|
1dcae6057c | ||
|
|
648f174bd8 | ||
|
|
f20fe9a075 | ||
|
|
023b4862eb | ||
|
|
a2d278fa4d | ||
|
|
f6df9d1988 | ||
|
|
39ce35b219 | ||
|
|
33c08119ec | ||
|
|
1475fa24d1 | ||
|
|
348d2e0a01 | ||
|
|
65fc526220 | ||
|
|
4c4f5643f4 | ||
|
|
634304dfce | ||
|
|
70980afab0 | ||
|
|
bb4659a2dd | ||
|
|
59df7fdc25 | ||
|
|
2d4977ba8f | ||
|
|
b27bcd2d35 | ||
|
|
2f006d3e3b | ||
|
|
d71eec6a5e | ||
|
|
3a977ded02 | ||
|
|
b14587cdf1 | ||
|
|
5f8862b4d2 | ||
|
|
f3201cfd97 | ||
|
|
d94645d381 | ||
|
|
cd1cecd774 | ||
|
|
a87c1ae9f4 | ||
|
|
c271600432 | ||
|
|
1fb14db6f3 | ||
|
|
7b2be01430 | ||
|
|
9d9e3c4957 | ||
|
|
fc6cded59e | ||
|
|
2ec3a0b202 | ||
|
|
88326ed573 | ||
|
|
58fc8866f1 | ||
|
|
74a5f9bf2c | ||
|
|
690d45ff9a | ||
|
|
34757cf535 | ||
|
|
69c83d33b1 | ||
|
|
55d48057da | ||
|
|
e7031e9c0d | ||
|
|
e99f0c07ba | ||
|
|
e526fa812e | ||
|
|
3dbb963e4e | ||
|
|
126b60f5dd | ||
|
|
6a69a1c1bf | ||
|
|
750d10c01d | ||
|
|
a51991ef0d | ||
|
|
011baad9d0 | ||
|
|
ec9f9d241e | ||
|
|
b8fa22eede | ||
|
|
a8c05a3d27 | ||
|
|
39f9a02c0a | ||
|
|
e47ad59003 | ||
|
|
0d7e958505 | ||
|
|
9812db5cd6 | ||
|
|
12ab456446 | ||
|
|
206ec7fdab | ||
|
|
da557a5c16 | ||
|
|
fba971b419 | ||
|
|
2e2e7be633 | ||
|
|
8a54a88b0a | ||
|
|
5d128c6cbd | ||
|
|
b09733175e | ||
|
|
d3d4b5a632 | ||
|
|
5b85eec64c | ||
|
|
0cf599bbe2 | ||
|
|
3f1947a4d9 | ||
|
|
3e26e3d420 | ||
|
|
51bc32aad0 | ||
|
|
70cbe9d11e | ||
|
|
c95bfc400b | ||
|
|
678754d251 | ||
|
|
58b618cf5f | ||
|
|
22da074965 | ||
|
|
9ae1cd4aff | ||
|
|
9887eb4804 | ||
|
|
7f4eb85106 | ||
|
|
896baf08d1 | ||
|
|
e1d429877c | ||
|
|
b9f3955767 | ||
|
|
f9a43e53e2 | ||
|
|
442da1ffc6 | ||
|
|
57970465b1 | ||
|
|
d203fe7e57 | ||
|
|
abdebfccad | ||
|
|
eb238b6902 | ||
|
|
edfde1aa9f | ||
|
|
912135877c | ||
|
|
597bf89135 | ||
|
|
e4c95cb396 | ||
|
|
867bc0ca29 | ||
|
|
35198b6cc3 | ||
|
|
5b2ebac15e | ||
|
|
4a3144d360 | ||
|
|
25bee7ab95 | ||
|
|
1c13a38ccb | ||
|
|
bbf2a2eb55 | ||
|
|
01a28f4d53 | ||
|
|
f5113da9c2 | ||
|
|
db9270b7ce | ||
|
|
a4a92c18cb | ||
|
|
eb5d5616bb | ||
|
|
04261355b7 | ||
|
|
0bb106028b | ||
|
|
431cd2e867 | ||
|
|
cdbf6ed6b4 | ||
|
|
87f132a684 | ||
|
|
5d79efbf15 | ||
|
|
118c68c9da | ||
|
|
8ee4c112cf | ||
|
|
2bc4d904c6 | ||
|
|
a690265f70 | ||
|
|
2b4bc7aa43 | ||
|
|
9041301642 | ||
|
|
9f088ba463 | ||
|
|
e7b9059958 | ||
|
|
e407a3c002 | ||
|
|
6be2ced06f | ||
|
|
95bdb570f2 | ||
|
|
9aa686b4a4 | ||
|
|
b4cdd3e1ef | ||
|
|
96fa776bd9 | ||
|
|
aa43a848a1 | ||
|
|
335a1abf41 | ||
|
|
b0647171d8 | ||
|
|
ac233a5920 | ||
|
|
0cc335ea9b | ||
|
|
1336ffc437 | ||
|
|
9186e457d9 | ||
|
|
8dbb0468f8 | ||
|
|
246a677517 | ||
|
|
644ef8d189 | ||
|
|
daa0d788af | ||
|
|
93a1c7a242 | ||
|
|
31583104bd | ||
|
|
bb8a3f4dfc | ||
|
|
150ab45748 | ||
|
|
b902713847 | ||
|
|
fd2fc92b72 | ||
|
|
0d6c8aa261 | ||
|
|
580dbb88e6 | ||
|
|
9fa22a0469 | ||
|
|
180cbb9503 | ||
|
|
567da606f3 | ||
|
|
182ec78f79 | ||
|
|
4cfacb2581 | ||
|
|
badcb96c6b | ||
|
|
224eb1d3ba | ||
|
|
709c653afb | ||
|
|
32155ba98c | ||
|
|
075779f107 | ||
|
|
50e9ac61f8 | ||
|
|
7bd3cb44f8 | ||
|
|
876dc94cc9 | ||
|
|
74677c89a2 | ||
|
|
484c50c921 | ||
|
|
0ae5065aaf | ||
|
|
5677855a43 | ||
|
|
0376d48bb9 | ||
|
|
63ce7e9fa0 | ||
|
|
d89e080b36 | ||
|
|
000bf4c027 | ||
|
|
1d1f62e38e | ||
|
|
b255af5f0f | ||
|
|
799a32544b | ||
|
|
5afa5d953c | ||
|
|
d2ee8be7e1 | ||
|
|
68346e075a | ||
|
|
b5470c873b | ||
|
|
81cdeea761 | ||
|
|
3d02143422 | ||
|
|
283bdcc56d | ||
|
|
01977170f1 | ||
|
|
b88ace2117 | ||
|
|
078c79d8ee | ||
|
|
696441c39b | ||
|
|
802ea61639 | ||
|
|
2ea15df9d1 | ||
|
|
ff492fc1c7 | ||
|
|
dcd9d42d32 | ||
|
|
454995304a | ||
|
|
8dd8e2e749 | ||
|
|
02160e1e8e | ||
|
|
2d7df96ad9 | ||
|
|
f83d1b395f | ||
|
|
53da1e49a0 | ||
|
|
7cc989014a | ||
|
|
725e877f1b | ||
|
|
e856f8208e | ||
|
|
e851593c12 | ||
|
|
80b12a6e3a | ||
|
|
00c0ae801a | ||
|
|
9dca2eb1c1 | ||
|
|
c9e3d3a708 | ||
|
|
2b2080c232 | ||
|
|
fd7954815a | ||
|
|
879e63157d | ||
|
|
d8c80928b0 | ||
|
|
b6cb22ed2d | ||
|
|
a06e71291f | ||
|
|
77ee959c43 | ||
|
|
cd9a4794ac | ||
|
|
336501aaff | ||
|
|
8883df3445 | ||
|
|
7178313c2e | ||
|
|
5e14ad0829 | ||
|
|
39cbdc749b | ||
|
|
cedad90513 | ||
|
|
7f29fd5125 | ||
|
|
67d5d76d67 | ||
|
|
43477d33cc | ||
|
|
d8998a73b3 | ||
|
|
625db5e26b | ||
|
|
ad0e9c1d4d | ||
|
|
caf944716b | ||
|
|
8162a182d7 | ||
|
|
22f3fb6e3e | ||
|
|
6d68b939be | ||
|
|
5c673a6a01 | ||
|
|
2497627c1d | ||
|
|
cbaa79b697 | ||
|
|
e4bf8f5e81 | ||
|
|
73cbb9dbb3 | ||
|
|
2a3fb0fd84 | ||
|
|
8d0f2fca51 | ||
|
|
6e88e7030d | ||
|
|
0b769a6524 | ||
|
|
3b1bb23563 | ||
|
|
1479832b51 | ||
|
|
438a9135e4 | ||
|
|
1bfba591ea | ||
|
|
de71b8f67c | ||
|
|
3f310a9aaf | ||
|
|
68da3ce2c1 | ||
|
|
3e952e3b36 | ||
|
|
3819e4313d | ||
|
|
76542a8032 | ||
|
|
90e4d84865 | ||
|
|
754436c79d | ||
|
|
b0bdb2c6f6 | ||
|
|
5a045fd89d | ||
|
|
0100836e31 | ||
|
|
53eaa2e0f4 | ||
|
|
9e968479fc | ||
|
|
23b75f56b5 | ||
|
|
21608b740a | ||
|
|
62ec8159b8 | ||
|
|
8e558d2342 | ||
|
|
477a557eb8 | ||
|
|
78a901f2f6 | ||
|
|
6a1730bd2b | ||
|
|
7507b90561 | ||
|
|
75cd72d028 | ||
|
|
066c451f0c | ||
|
|
9ab234f6d1 | ||
|
|
92c7871659 | ||
|
|
ff4a453ea9 | ||
|
|
6ed315a7ae | ||
|
|
005dbdebec | ||
|
|
50a00e1028 | ||
|
|
17dfdac0c1 | ||
|
|
e30f870e92 | ||
|
|
f176bbe090 | ||
|
|
70e55a039d | ||
|
|
207eb6790a | ||
|
|
2e3f9b9e25 | ||
|
|
b01c08b0d2 | ||
|
|
5def8dfd42 | ||
|
|
5a4588ac7f | ||
|
|
744016972e | ||
|
|
0531e2cb54 | ||
|
|
9671505a49 | ||
|
|
084c4afdc3 | ||
|
|
43376b8214 | ||
|
|
426c3549c6 | ||
|
|
fc7c74f3e1 | ||
|
|
37dc0278fa | ||
|
|
8f8dae70c2 | ||
|
|
8fec470e9f | ||
|
|
7417499d4b | ||
|
|
726945c2ab | ||
|
|
98d1b0643d | ||
|
|
0da15443cb | ||
|
|
b1dcd46a33 | ||
|
|
7ef8d8d1ff | ||
|
|
f9c3f5ed25 | ||
|
|
fc912279b6 | ||
|
|
c0b961ee39 | ||
|
|
1ed1f3a2b6 | ||
|
|
2baf11fc11 | ||
|
|
f93f5e30d8 | ||
|
|
c56cf6f4ce | ||
|
|
e95d47cfdf | ||
|
|
bae5ba9d20 | ||
|
|
f84d4cacee | ||
|
|
e91a264768 | ||
|
|
ce210f8805 | ||
|
|
0845575680 | ||
|
|
29999bc2ce | ||
|
|
87e43ab7ce | ||
|
|
0773915fbc | ||
|
|
41862257ac | ||
|
|
3cebb100c7 | ||
|
|
7a98afa8df | ||
|
|
bbab35e05b | ||
|
|
473f8d667c | ||
|
|
8fa779bb32 | ||
|
|
52bf8922c4 | ||
|
|
df5fc0a100 | ||
|
|
b7ee282238 | ||
|
|
2e6afa86f6 | ||
|
|
69a34301e7 | ||
|
|
8887092606 | ||
|
|
0e20a06731 | ||
|
|
ec6919d499 | ||
|
|
3e0f17ee3c | ||
|
|
13f769af5e | ||
|
|
35a1d84951 | ||
|
|
13b5d9c108 | ||
|
|
7f588ebedf | ||
|
|
142bf2c890 | ||
|
|
51589f6c0a | ||
|
|
155923b2e7 | ||
|
|
b65c4530b2 | ||
|
|
d3cb93b218 | ||
|
|
5083d8e7f8 | ||
|
|
da81942adc | ||
|
|
d781c94027 | ||
|
|
c142860433 | ||
|
|
e591eb8ff8 | ||
|
|
f032dd1e90 | ||
|
|
e9583166f4 | ||
|
|
cd94f11374 | ||
|
|
6b4ff7aa61 | ||
|
|
3c18a24cb6 | ||
|
|
6ad731d158 | ||
|
|
81f7659bbc | ||
|
|
ab084e0d5a | ||
|
|
02460544bf | ||
|
|
927ba29e04 | ||
|
|
be2eb8215a | ||
|
|
11660a852f | ||
|
|
ec68b8827c | ||
|
|
d93326f672 | ||
|
|
6fcbf58af5 | ||
|
|
8f60a29fc7 | ||
|
|
eeb8071fec | ||
|
|
9e4527245d | ||
|
|
18f78a463d | ||
|
|
15732a9ed7 | ||
|
|
8c8bd22664 | ||
|
|
e67eba61aa | ||
|
|
7cf4f801f4 | ||
|
|
a0b588000a | ||
|
|
7caf857c98 | ||
|
|
2e91c74baf | ||
|
|
c38f94558a | ||
|
|
c86a9d39f2 | ||
|
|
563700d995 | ||
|
|
e433648fe6 | ||
|
|
4a351385e1 | ||
|
|
b868ab292b | ||
|
|
77d0fcb336 | ||
|
|
a2327b6169 | ||
|
|
913045fe12 | ||
|
|
c049a1d3db | ||
|
|
bd7aa4cc4e | ||
|
|
d7aaeda187 | ||
|
|
bd58e6c7cb | ||
|
|
32fa22efcd | ||
|
|
f629833229 | ||
|
|
d3f7b40a9b | ||
|
|
8ed814ac9f | ||
|
|
90f6808663 | ||
|
|
644e431eba | ||
|
|
31b1eb594e | ||
|
|
315492a08a | ||
|
|
027b45e6c3 | ||
|
|
8a5728508c | ||
|
|
a08426e078 | ||
|
|
7749a14da3 | ||
|
|
053b1d642c | ||
|
|
53985dd250 | ||
|
|
cc8166183e | ||
|
|
9bca10b380 | ||
|
|
c50ffc5c8b | ||
|
|
277dc6c008 | ||
|
|
81ee147f51 | ||
|
|
f0d32d7754 | ||
|
|
aba9b2e19e | ||
|
|
67324f0d0c | ||
|
|
7070ac21d4 | ||
|
|
2d828d7739 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "web/src/cascade"]
|
||||
path = app/web/src/cascade
|
||||
url = https://github.com/Irev-Dev/CascadeStudio.git
|
||||
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Cadhub",
|
||||
"Customizer",
|
||||
"Hutten",
|
||||
"cadquery",
|
||||
"jscad",
|
||||
"openscad",
|
||||
"sendmail"
|
||||
]
|
||||
}
|
||||
67
CONTRIBUTING.md
Normal file
67
CONTRIBUTING.md
Normal file
@@ -0,0 +1,67 @@
|
||||
Hello 👋
|
||||
|
||||
|
||||
Really happy you're checking out how to contribute.
|
||||
Here you'll find a break down of the tech we're using,
|
||||
|
||||
If you'd like to get involved one of the best ways is to drop by the [discord](https://discord.gg/SD7zFRNjGH), say hi and let us know you're interested in contributing. All are welcome.
|
||||
|
||||
## Tech used
|
||||
|
||||
### Redwood
|
||||
CadHub is a [RedWood app](https://redwoodjs.com/). Simplistically this means it's a React frontend, using a serverless graphQL backend with Prisma.
|
||||
We are also using [Tailwind](https://tailwindcss.com/) to style the app.
|
||||
To learn more about Redwood, here are some useful links:
|
||||
- [Tutorial](https://redwoodjs.com/tutorial/welcome-to-redwood): getting started and complete overview guide.
|
||||
- [Docs](https://redwoodjs.com/docs/introduction): using the Redwood Router, handling assets and files, list of command-line tools, and more.
|
||||
- [Redwood Community](https://community.redwoodjs.com): get help, share tips and tricks, and collaborate on everything about RedwoodJS.
|
||||
|
||||
### Cad Packages
|
||||
Because Each CadPackage is it's own beast we opted to use Docker in order to give us lots of flexibility for setting up the environment to run there packages. The containers are run using AWS's container lambda and deployed using the serverless framework (JSCAD is an exception since it runs client-side). See [our docs](https://learn.cadhub.xyz/docs/general-cadhub/integrations) for more information of how this is setup.
|
||||
|
||||
## Getting your dev environment setup
|
||||
|
||||
|
||||
Clone the repo and `cd` in the app directory (the docs directory is for [learn.cadhub](https://learn.cadhub.xyz/))
|
||||
```
|
||||
cd app
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
```terminal
|
||||
yarn install
|
||||
```
|
||||
|
||||
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup).
|
||||
|
||||
Run the following
|
||||
``` terminal
|
||||
yarn rw prisma migrate dev
|
||||
yarn rw prisma db seed
|
||||
```
|
||||
|
||||
p.s. `yarn rw prisma studio` spins up an app to inspect the db
|
||||
|
||||
### Fire up dev
|
||||
```terminal
|
||||
yarn rw dev
|
||||
```
|
||||
|
||||
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
|
||||
|
||||
If you want to access the websight on your phone use `yarn redwood dev --fwd="--host <ip-address-on-your-network-i.e.-192.168.0.5>"`
|
||||
|
||||
you can sign in to the following accounts locally
|
||||
|
||||
localUser1@kurthutten.com: `abc123`
|
||||
|
||||
localUser2@kurthutten.com: `abc123`
|
||||
|
||||
localAdmin@kurthutten.com: `abc123`
|
||||
|
||||
## Designs
|
||||
|
||||
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
|
||||
|
||||
## Docs
|
||||
Docs are hosted at [learn.cadhub.xyz](http://learn.cadhub.xyz/). It includes a OpenSCAD tutorial at this point, and more is coming. The docs can be found in this repo at [docs](https://github.com/Irev-Dev/cadhub/tree/main/docs)
|
||||
79
README.md
79
README.md
@@ -1,78 +1,17 @@
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
# [C a d H u b](https://cadhub.xyz)
|
||||
|
||||
[](https://app.netlify.com/sites/cadhubxyz/deploys)
|
||||
<!-- [](https://app.netlify.com/sites/cadhubxyz/deploys) -->
|
||||
|
||||
Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have an integration with the excellent [cascadeStudio](https://zalo.github.io/CascadeStudio/) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad).
|
||||
Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have integrations in progress for [OpenSCAD](https://cadhub.xyz/dev-ide/openscad) and [CadQuery](https://cadhub.xyz/dev-ide/cadquery) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad).
|
||||
|
||||
If you want to be involved in anyway, checkout the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and get in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
|
||||
If you want to be involved in anyway, checkout the [contributing.md](https://github.com/Irev-Dev/cadhub/blob/main/CONTRIBUTING.md).
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/fullcadhubshot.jpg">
|
||||
you might also be interested in the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and getting in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/Part%20IDE%20-%20export%20expand%20state.jpg">
|
||||
## Who is CadHub
|
||||
|
||||
## Getting Started
|
||||
|
||||
Because we're integrating cascadeStudio, this is done some what crudely for the time being, so you'll need to clone the repo with submodules.
|
||||
|
||||
```terminal
|
||||
git clone --recurse-submodules -j8 git@github.com:Irev-Dev/cadhub.git
|
||||
# or
|
||||
git clone --recurse-submodules -j8 https://github.com/Irev-Dev/cadhub.git
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
```terminal
|
||||
yarn install
|
||||
```
|
||||
|
||||
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup) with a couple of exceptions:
|
||||
- Run `yarn rw prisma migrate dev` instead of `yarn rw db up` in the guide.
|
||||
- Don't worry about changing the `schema.prisma` file.
|
||||
- You will need to add a `DATABASE_URL` and test url to you `.env` file at the root of the project.
|
||||
|
||||
Run the following
|
||||
``` terminal
|
||||
yarn rw prisma migrate dev
|
||||
yarn rw prisma db seed
|
||||
```
|
||||
|
||||
### Fire up dev
|
||||
```terminal
|
||||
yarn rw dev
|
||||
```
|
||||
|
||||
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
|
||||
|
||||
you can sign in to the following accounts locally
|
||||
|
||||
localUser1@kurthutten.com: `abc123`
|
||||
|
||||
localUser2@kurthutten.com: `abc123`
|
||||
|
||||
localAdmin@kurthutten.com: `abc123`
|
||||
|
||||
You may need to register a account depending on what issue you are trying to tackle, This can be done by clicking the login button on the top right. This will open up netlify's idenitiy modal asking for the websites url, since it will notice you developing locally. Enter `https://cadhub.xyz/` than use you email, verify your email and you should be set.
|
||||
(some routes are protected, but permissions is a big area that needs a lot of work in the near future, so it's in a very incomplete state atm)
|
||||
|
||||
### Note:
|
||||
We're using [RedwoodJS](https://redwoodjs.com/), this is perhaps unwise since they haven't reached 1.0 yet, however with their aim to release 1.0 by the end of the year, it shouldn't be too difficult to port changes over the coming months.
|
||||
If you not familiar with Redwood, never fear the main bit of tech it uses is React, Graphql(apollo) and serverless/lamdas, depending on what part of the app you want to help with, so long as you know you way around these bits of tech you should be fine with some light referencing of the RedWood docs
|
||||
|
||||
### Extra Redwood docs, i.e. getting familiar with the frame work.
|
||||
- [Tutorial](https://redwoodjs.com/tutorial/welcome-to-redwood): getting started and complete overview guide.
|
||||
- [Docs](https://redwoodjs.com/docs/introduction): using the Redwood Router, handling assets and files, list of command-line tools, and more.
|
||||
- [Redwood Community](https://community.redwoodjs.com): get help, share tips and tricks, and collaborate on everything about RedwoodJS.
|
||||
|
||||
## Styles
|
||||
|
||||
We're using tailwind utility classes so please try and use them as much as possible. Again if you not familiar, the [tailwind search](https://tailwindcss.com/) is fantastic, so searching for the css property you want to use will lead you to the correct class 99% of the time.
|
||||
|
||||
## Designs
|
||||
|
||||
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/Part%20Page(1).jpg">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/User%20Page%20Edit.jpg">
|
||||
[Kurt](https://github.com/Irev-Dev) and [Frank](https://github.com/franknoirot) make up the Core-team and [Jeremy](https://github.com/jmwright), [Torsten](https://github.com/t-paul) and [Hrg](https://github.com/hrgdavor) are a major contributors. Plus a number smaller contributors.
|
||||
|
||||
@@ -17,3 +17,14 @@ CLOUDINARY_API_KEY=476712943135152
|
||||
# See: https://redwoodjs.com/docs/logger for level options:
|
||||
# trace | info | debug | warn | error | silent
|
||||
# LOG_LEVEL=debug
|
||||
|
||||
|
||||
# EMAIL_PASSWORD=abc123
|
||||
|
||||
|
||||
# CAD_LAMBDA_BASE_URL="http://localhost:8080"
|
||||
|
||||
# sentry
|
||||
GITHUB_ASSIST_APP_ID=23342
|
||||
GITHUB_ASSIST_SECRET=abc
|
||||
GITHUB_ASSIST_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nabcdefg\n-----END RSA PRIVATE KEY-----"
|
||||
|
||||
@@ -1 +1 @@
|
||||
/web/src/cascade/*
|
||||
|
||||
|
||||
7
app/.gitignore
vendored
Normal file
7
app/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
dist
|
||||
web/types/graphql.d.ts
|
||||
api/types/graphql.d.ts
|
||||
|
||||
|
||||
# Deployment
|
||||
.serverless
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = { extends: "../babel.config.js" }
|
||||
2547
app/api/backend.tldr
Normal file
2547
app/api/backend.tldr
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "RW_DataMigration" (
|
||||
"version" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"startedAt" TIMESTAMP(3) NOT NULL,
|
||||
"finishedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY ("version")
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `Comment` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Part` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `PartReaction` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_partId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Part" DROP CONSTRAINT "Part_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PartReaction" DROP CONSTRAINT "PartReaction_partId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PartReaction" DROP CONSTRAINT "PartReaction_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Comment";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Part";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "PartReaction";
|
||||
@@ -0,0 +1,59 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Project" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" VARCHAR(25) NOT NULL,
|
||||
"description" TEXT,
|
||||
"code" TEXT,
|
||||
"mainImage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProjectReaction" (
|
||||
"id" TEXT NOT NULL,
|
||||
"emote" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Comment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Project.title_userId_unique" ON "Project"("title", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProjectReaction.emote_userId_projectId_unique" ON "ProjectReaction"("emote", "userId", "projectId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Project" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectReaction" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectReaction" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CadPackage" AS ENUM ('openscad', 'cadquery');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "cadPackage" "CadPackage" NOT NULL DEFAULT E'openscad';
|
||||
@@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "SocialCard" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"url" TEXT,
|
||||
"outOfDate" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SocialCard_projectId_unique" ON "SocialCard"("projectId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SocialCard" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterIndex
|
||||
ALTER INDEX "SocialCard_projectId_unique" RENAME TO "SocialCard.projectId_unique";
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "CadPackage" ADD VALUE 'jscad';
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "forkedFromId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Project" ADD FOREIGN KEY ("forkedFromId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
56
app/api/db/migrations/20211113002346_prisma_v3/migration.sql
Normal file
56
app/api/db/migrations/20211113002346_prisma_v3/migration.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_projectId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ProjectReaction" DROP CONSTRAINT "ProjectReaction_projectId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ProjectReaction" DROP CONSTRAINT "ProjectReaction_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "SocialCard" DROP CONSTRAINT "SocialCard_projectId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "SubjectAccessRequest" DROP CONSTRAINT "SubjectAccessRequest_userId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SocialCard" ADD CONSTRAINT "SocialCard_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectReaction" ADD CONSTRAINT "ProjectReaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectReaction" ADD CONSTRAINT "ProjectReaction_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SubjectAccessRequest" ADD CONSTRAINT "SubjectAccessRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Project.title_userId_unique" RENAME TO "Project_title_userId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "ProjectReaction.emote_userId_projectId_unique" RENAME TO "ProjectReaction_emote_userId_projectId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "SocialCard.projectId_unique" RENAME TO "SocialCard_projectId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "User.email_unique" RENAME TO "User_email_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "User.userName_unique" RENAME TO "User_userName_key";
|
||||
@@ -1,2 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,11 +1,11 @@
|
||||
datasource DS {
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
binaryTargets = ["native", "rhel-openssl-1.0.x"]
|
||||
}
|
||||
|
||||
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
|
||||
@@ -14,11 +14,6 @@ generator client {
|
||||
// ADMIN
|
||||
// }
|
||||
|
||||
// enum PartType {
|
||||
// CASCADESTUDIO
|
||||
// JSCAD
|
||||
// }
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
userName String @unique // reffered to as userId in @relations
|
||||
@@ -33,15 +28,21 @@ model User {
|
||||
|
||||
image String? // url maybe id or file storage service? cloudinary?
|
||||
bio String? //mark down
|
||||
Part Part[]
|
||||
Reaction PartReaction[]
|
||||
Project Project[]
|
||||
Reaction ProjectReaction[]
|
||||
Comment Comment[]
|
||||
SubjectAccessRequest SubjectAccessRequest[]
|
||||
}
|
||||
|
||||
model Part {
|
||||
enum CadPackage {
|
||||
openscad
|
||||
cadquery
|
||||
jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
title String @db.VarChar(25)
|
||||
description String? // markdown string
|
||||
code String?
|
||||
mainImage String? // link to cloudinary
|
||||
@@ -50,23 +51,39 @@ model Part {
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
deleted Boolean @default(false)
|
||||
cadPackage CadPackage @default(openscad)
|
||||
socialCard SocialCard?
|
||||
forkedFromId String?
|
||||
forkedFrom Project? @relation("Fork", fields: [forkedFromId], references: [id])
|
||||
|
||||
Comment Comment[]
|
||||
Reaction PartReaction[]
|
||||
childForks Project[] @relation("Fork")
|
||||
Comment Comment[]
|
||||
Reaction ProjectReaction[]
|
||||
@@unique([title, userId])
|
||||
}
|
||||
|
||||
model PartReaction {
|
||||
model SocialCard {
|
||||
id String @id @default(uuid())
|
||||
projectId String @unique
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
url String? // link to cloudinary
|
||||
outOfDate Boolean @default(true)
|
||||
}
|
||||
|
||||
model ProjectReaction {
|
||||
id String @id @default(uuid())
|
||||
emote String // an emoji
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
projectId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@unique([emote, userId, partId])
|
||||
@@unique([emote, userId, projectId])
|
||||
}
|
||||
|
||||
model Comment {
|
||||
@@ -74,8 +91,8 @@ model Comment {
|
||||
text String // the comment, should I allow mark down?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
part Part @relation(fields: [partId], references: [id])
|
||||
partId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
projectId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -91,3 +108,10 @@ model SubjectAccessRequest {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model RW_DataMigration {
|
||||
version String @id
|
||||
name String
|
||||
startedAt DateTime
|
||||
finishedAt DateTime
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
const dotenv = require('dotenv')
|
||||
|
||||
dotenv.config()
|
||||
const db = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
// Seed data is database data that needs to exist for your app to run.
|
||||
// Ideally this file should be idempotent: running it multiple times
|
||||
// will result in the same database state (usually by checking for the
|
||||
// existence of a record before trying to create it). For example:
|
||||
//
|
||||
// const existing = await db.user.findMany({ where: { email: 'admin@email.com' }})
|
||||
// if (!existing.length) {
|
||||
// await db.user.create({ data: { name: 'Admin', email: 'admin@email.com' }})
|
||||
// }
|
||||
const users = [
|
||||
{
|
||||
id: "a2b21ce1-ae57-43a2-b6a3-b6e542fd9e60",
|
||||
userName: "local-user-1",
|
||||
name: "local 1",
|
||||
email: "localUser1@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "682ba807-d10e-4caf-bf28-74054e46c9ec",
|
||||
userName: "local-user-2",
|
||||
name: "local 2",
|
||||
email: "localUser2@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "5cea3906-1e8e-4673-8f0d-89e6a963c096",
|
||||
userName: "local-admin-2",
|
||||
name: "local admin",
|
||||
email: "localAdmin@kurthutten.com"
|
||||
},
|
||||
]
|
||||
|
||||
let existing
|
||||
existing = await db.user.findMany({ where: { id: users[0].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[0],
|
||||
})
|
||||
}
|
||||
existing = await db.user.findMany({ where: { id: users[1].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[1],
|
||||
})
|
||||
}
|
||||
|
||||
const parts = [
|
||||
{
|
||||
title: 'demo-part1',
|
||||
description: '# can be markdown',
|
||||
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[0].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'demo-part2',
|
||||
description: '## [hey](www.google.com)',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[1].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
existing = await db.part.findMany({where: { title: parts[0].title}})
|
||||
if(!existing.length) {
|
||||
await db.part.create({
|
||||
data: parts[0],
|
||||
})
|
||||
}
|
||||
existing = await db.part.findMany({where: { title: parts[1].title}})
|
||||
if(!existing.length) {
|
||||
await db.part.create({
|
||||
data: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const aPart = await db.part.findUnique({where: {
|
||||
title_userId: {
|
||||
title: parts[0].title,
|
||||
userId: users[0].id,
|
||||
}
|
||||
}})
|
||||
await db.comment.create({
|
||||
data: {
|
||||
text: "nice part, I like it",
|
||||
user: {connect: { id: users[0].id}},
|
||||
part: {connect: { id: aPart.id}},
|
||||
}
|
||||
})
|
||||
await db.partReaction.create({
|
||||
data: {
|
||||
emote: "❤️",
|
||||
user: {connect: { id: users[0].id}},
|
||||
part: {connect: { id: aPart.id}},
|
||||
}
|
||||
})
|
||||
|
||||
console.info('No data to seed. See api/prisma/seeds.js for info.')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => console.error(e))
|
||||
.finally(async () => {
|
||||
await db.$disconnect()
|
||||
})
|
||||
@@ -1,6 +1 @@
|
||||
const { getConfig } = require('@redwoodjs/core')
|
||||
|
||||
const config = getConfig({ type: 'jest', target: 'node' })
|
||||
config.displayName.name = 'api'
|
||||
|
||||
module.exports = config
|
||||
module.exports = require('@redwoodjs/testing/config/jest/api')
|
||||
|
||||
@@ -3,8 +3,25 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@redwoodjs/api": "^0.31.0",
|
||||
"@redwoodjs/api-server": "^0.31.0",
|
||||
"cloudinary": "^1.23.0"
|
||||
"@redwoodjs/api": "^0.38.1",
|
||||
"@redwoodjs/graphql-server": "^0.38.1",
|
||||
"@sentry/node": "^6.5.1",
|
||||
"axios": "^0.21.1",
|
||||
"cloudinary": "^1.23.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"human-id": "^2.0.1",
|
||||
"middy": "^0.36.0",
|
||||
"nanoid": "^3.1.20",
|
||||
"nodemailer": "^6.6.2",
|
||||
"serverless-binary-cors": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@netlify/zip-it-and-ship-it": "^4.30.0",
|
||||
"@types/nodemailer": "^6.4.2",
|
||||
"concurrently": "^6.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"serverless-dotenv-plugin": "^3.10.0",
|
||||
"serverless-plugin-git-variables": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
18
app/api/src/directives/requireAuth/requireAuth.test.ts
Normal file
18
app/api/src/directives/requireAuth/requireAuth.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { mockRedwoodDirective, getDirectiveName } from '@redwoodjs/testing/api'
|
||||
|
||||
import requireAuth from './requireAuth'
|
||||
|
||||
describe('requireAuth directive', () => {
|
||||
it('declares the directive sdl as schema, with the correct name', () => {
|
||||
expect(requireAuth.schema).toBeTruthy()
|
||||
expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
|
||||
})
|
||||
|
||||
it('requireAuth has stub implementation. Should not throw when current user', () => {
|
||||
// If you want to set values in context, pass it through e.g.
|
||||
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
|
||||
const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
|
||||
|
||||
expect(mockExecution).not.toThrowError()
|
||||
})
|
||||
})
|
||||
22
app/api/src/directives/requireAuth/requireAuth.ts
Normal file
22
app/api/src/directives/requireAuth/requireAuth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { createValidatorDirective } from '@redwoodjs/graphql-server'
|
||||
|
||||
import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
|
||||
|
||||
export const schema = gql`
|
||||
"""
|
||||
Use to check whether or not a user is authenticated and is associated
|
||||
with an optional set of roles.
|
||||
"""
|
||||
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
|
||||
`
|
||||
|
||||
const validate = ({ directiveArgs }) => {
|
||||
const { roles } = directiveArgs
|
||||
applicationRequireAuth({ roles })
|
||||
}
|
||||
|
||||
const requireAuth = createValidatorDirective(schema, validate)
|
||||
|
||||
export default requireAuth
|
||||
10
app/api/src/directives/skipAuth/skipAuth.test.ts
Normal file
10
app/api/src/directives/skipAuth/skipAuth.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getDirectiveName } from '@redwoodjs/testing/api'
|
||||
|
||||
import skipAuth from './skipAuth'
|
||||
|
||||
describe('skipAuth directive', () => {
|
||||
it('declares the directive sdl as schema, with the correct name', () => {
|
||||
expect(skipAuth.schema).toBeTruthy()
|
||||
expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
|
||||
})
|
||||
})
|
||||
16
app/api/src/directives/skipAuth/skipAuth.ts
Normal file
16
app/api/src/directives/skipAuth/skipAuth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { createValidatorDirective } from '@redwoodjs/graphql-server'
|
||||
|
||||
export const schema = gql`
|
||||
"""
|
||||
Use to skip authentication checks and allow public access.
|
||||
"""
|
||||
directive @skipAuth on FIELD_DEFINITION
|
||||
`
|
||||
|
||||
const skipAuth = createValidatorDirective(schema, () => {
|
||||
return
|
||||
})
|
||||
|
||||
export default skipAuth
|
||||
5
app/api/src/docker/.env.example
Normal file
5
app/api/src/docker/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# The following are the env vars you need run the cad lamdas locally
|
||||
# The still connect to s3 so some secrets are needed, ask Kurt and he'll set things up for you
|
||||
DEV_AWS_SECRET_ACCESS_KEY=""
|
||||
DEV_AWS_ACCESS_KEY_ID=""
|
||||
DEV_BUCKET="cad-preview-bucket-dev-001"
|
||||
@@ -1,11 +1,12 @@
|
||||
# Serverless
|
||||
|
||||
We're using the serverless from work for deployment
|
||||
We're using the serverless framework for deployment
|
||||
|
||||
```
|
||||
sls deploy
|
||||
yarn rw build api && sls deploy --stage <stagename>
|
||||
```
|
||||
But [Kurt Hutten](https://github.com/Irev-Dev) is the only one with credentials for deployment atm, though if you wanted to set your own account you could deploy to that if you wanted to test.
|
||||
Deploying has `yarn rw build` first because the image uses built js files
|
||||
|
||||
## Testing changes locally
|
||||
|
||||
@@ -14,20 +15,21 @@ You'll need to have Docker installed
|
||||
Because of the way the docker containers to be deployed as lambdas on aws are somewhat specialised for the purpose we're using `docker-compose` to spin one up for each function/endpoint. So we've added a aws-emulation layer
|
||||
|
||||
|
||||
Then cd into this folder `cd api/src/docker` and:
|
||||
The docker build relies on a git ignored file, the aws-lambda-rie. [Download it](https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie), then put it into `app/api/src/docker/common/`. Alternatively you can put this download into the DockerFiles by reading the instructions at around line 29 of the DockerFiles (`app/api/src/docker/openscad/Dockerfile` & `app/api/src/docker/cadquery/Dockerfile`). However this will mean slower build times as it will need download this 14mb file every build.
|
||||
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
yarn cad
|
||||
```
|
||||
The first time you run this, it has to build the main image it will take some time, but launching again will be quicker.
|
||||
|
||||
After which we'll also spin up a light express server to act as an emulator to transform some the request from the front end into how the lambda's expect them (This emulates the aws-api-gateway which changes tranforms the inbound requests somewhat).
|
||||
```
|
||||
yarn install
|
||||
yarn emulate
|
||||
yarn aws-emulate
|
||||
```
|
||||
You can now add OPENSCAD_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`)
|
||||
comment that line out if you want to go back to using the aws endpoint (and restart the dev process).
|
||||
You can now add CAD_LAMBDA_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`) comment that line out if you want to go back to using the aws endpoint (and restart the dev process).
|
||||
|
||||
If you change anything in the `api/src/docker/openscad` directory, you will need to stop the docker process and restart it (will be fairly quick if you're only changing the js)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const express = require('express')
|
||||
var cors = require('cors')
|
||||
const axios = require('axios')
|
||||
const { restart } = require('nodemon')
|
||||
const app = express()
|
||||
const port = 8080
|
||||
app.use(express.json())
|
||||
@@ -10,31 +9,32 @@ app.use(cors())
|
||||
const invocationURL = (port) =>
|
||||
`http://localhost:${port}/2015-03-31/functions/function/invocations`
|
||||
|
||||
app.post('/openscad/preview', async (req, res) => {
|
||||
try {
|
||||
const { data } = await axios.post(invocationURL(5052), {
|
||||
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
|
||||
})
|
||||
res.status(data.statusCode)
|
||||
res.send(data.body)
|
||||
} catch (e) {
|
||||
res.status(500)
|
||||
res.send()
|
||||
}
|
||||
})
|
||||
app.post('/cadquery/stl', async (req, res) => {
|
||||
console.log('making post request to 5060')
|
||||
try {
|
||||
const { data } = await axios.post(invocationURL(5060), {
|
||||
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
|
||||
})
|
||||
res.status(data.statusCode)
|
||||
res.send(data.body)
|
||||
} catch (e) {
|
||||
res.status(500)
|
||||
res.send()
|
||||
}
|
||||
})
|
||||
const makeRequest = (route, port) => [
|
||||
route,
|
||||
async (req, res) => {
|
||||
console.log(`making post request to ${port}, ${route}`)
|
||||
try {
|
||||
const { data } = await axios.post(invocationURL(port), {
|
||||
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
|
||||
})
|
||||
res.status(data.statusCode)
|
||||
res.setHeader('Content-Type', 'application/javascript')
|
||||
if (data.headers && data.headers['Content-Encoding'] === 'gzip') {
|
||||
res.setHeader('Content-Encoding', 'gzip')
|
||||
res.send(Buffer.from(data.body, 'base64'))
|
||||
} else {
|
||||
res.send(Buffer.from(data.body, 'base64'))
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500)
|
||||
res.send()
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
app.post(...makeRequest('/openscad/preview', 5052))
|
||||
app.post(...makeRequest('/openscad/stl', 5053))
|
||||
app.post(...makeRequest('/cadquery/stl', 5060))
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening at http://localhost:${port}`)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
FROM public.ecr.aws/lts/ubuntu:20.04_stable
|
||||
|
||||
FROM continuumio/miniconda3
|
||||
ENV PATH="/root/miniconda3/bin:${PATH}"
|
||||
ARG PATH="/root/miniconda3/bin:${PATH}"
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get update --fix-missing -qq
|
||||
RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get install -y wget
|
||||
|
||||
# install node14, see comment at the to of node14source_setup.sh
|
||||
ADD common/node14source_setup.sh /nodesource_setup.sh
|
||||
ADD api/src/docker/common/node14source_setup.sh /nodesource_setup.sh
|
||||
RUN ["chmod", "+x", "/nodesource_setup.sh"]
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs
|
||||
@@ -21,29 +22,45 @@ RUN apt-get update && \
|
||||
cmake \
|
||||
unzip \
|
||||
automake autoconf libtool \
|
||||
libcurl4-openssl-dev
|
||||
libcurl4-openssl-dev \
|
||||
curl \
|
||||
git
|
||||
|
||||
# Add the lambda emulator for local dev, (see entrypoint.sh for where it's used),
|
||||
# I have the file locally (gitignored) to speed up build times (as it downloads everytime),
|
||||
# but you can use the http version of the below ADD command or download it yourself from that url.
|
||||
ADD common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
ADD api/src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
# ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
RUN ["chmod", "+x", "/usr/local/bin/aws-lambda-rie"]
|
||||
|
||||
WORKDIR /var/task/
|
||||
COPY cadquery/package*.json /var/task/
|
||||
RUN npm install
|
||||
# aws-lambda-ric does not play nice with yarn, so installing it seperately,
|
||||
# circle back to this later for a proper solution
|
||||
COPY package*.json /var/task/
|
||||
RUN npm install aws-lambda-ric@1.0.0
|
||||
|
||||
RUN conda --version
|
||||
|
||||
# Install CadQuery
|
||||
RUN conda install -c cadquery -c conda-forge cadquery=master ocp=7.5.2 python=3.8
|
||||
RUN conda info
|
||||
|
||||
# Get a copy of cq-cli from GitHub
|
||||
RUN git clone https://github.com/CadQuery/cq-cli.git
|
||||
|
||||
# Get the distribution copy of cq-cli
|
||||
RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.1.0/cq-cli-Linux-x86_64.zip
|
||||
RUN unzip cq-cli-Linux-x86_64.zip
|
||||
RUN apt-get install -y libglew2.1
|
||||
|
||||
RUN chmod +x cq-cli/cq-cli
|
||||
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
|
||||
|
||||
# using built javascript from dist
|
||||
# run `yarn rw build` and $(npm bin)/zip-it-and-ship-it api/dist/functions/ api/dist/zipball before bulding this image
|
||||
COPY api/dist/zipball/cadquery.zip /var/task/
|
||||
# -n stops aws-lamda-ric from being overridden.
|
||||
RUN unzip -n /var/task/cadquery.zip
|
||||
|
||||
COPY api/src/docker/common/entrypoint.sh /entrypoint.sh
|
||||
|
||||
COPY cadquery/*.js /var/task/
|
||||
COPY common/*.js /var/common/
|
||||
COPY common/entrypoint.sh /entrypoint.sh
|
||||
RUN ["chmod", "+x", "/entrypoint.sh"]
|
||||
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||
CMD [ "cadquery.stl" ]
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
const { runCQ } = require('./runCQ')
|
||||
const middy = require('middy')
|
||||
const { cors } = require('middy/middlewares')
|
||||
|
||||
// cors true does not seem to work in serverless.yml, perhaps docker lambdas aren't covered by that config
|
||||
// special lambda just for responding to options requests
|
||||
const preflightOptions = (req, _context, callback) => {
|
||||
const response = {
|
||||
statusCode: 204,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
},
|
||||
}
|
||||
callback(null, response)
|
||||
}
|
||||
|
||||
const stl = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
console.log(eventBody, 'eventBody')
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, result, tempFile } = await runCQ({ file, settings })
|
||||
if (error) {
|
||||
const response = {
|
||||
statusCode: 400,
|
||||
body: JSON.stringify({ error, tempFile }),
|
||||
}
|
||||
callback(null, response)
|
||||
} else {
|
||||
console.log(`got result in route: ${result}, file is: ${tempFile}`)
|
||||
const fs = require('fs')
|
||||
const image = fs.readFileSync(`/tmp/${tempFile}/output.stl`, {
|
||||
encoding: 'base64',
|
||||
})
|
||||
console.log(image, 'encoded image')
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
imageBase64: image,
|
||||
result,
|
||||
tempFile,
|
||||
}),
|
||||
}
|
||||
callback(null, response)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stl: middy(stl).use(cors()),
|
||||
preflightOptions,
|
||||
}
|
||||
21
app/api/src/docker/cadquery/cadquery.ts
Normal file
21
app/api/src/docker/cadquery/cadquery.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { runCQ } from './runCQ'
|
||||
import middy from 'middy'
|
||||
import { cors } from 'middy/middlewares'
|
||||
import { loggerWrap, storeAssetAndReturnUrl } from '../common/utils'
|
||||
|
||||
const _stl = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
console.log('eventBody', eventBody)
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await runCQ({ file, settings })
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
})
|
||||
}
|
||||
|
||||
export const stl = middy(loggerWrap(_stl)).use(cors())
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "openscad-endpoint",
|
||||
"version": "0.0.1",
|
||||
"description": "endpoint for openscad",
|
||||
"main": "index.js",
|
||||
"author": "Kurt Hutten <kurt@kurthutten.com>",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"middy": "^0.36.0",
|
||||
"nanoid": "^3.1.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-lambda-ric": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const { makeFile, runCommand } = require('../common/utils')
|
||||
const { nanoid } = require('nanoid')
|
||||
|
||||
module.exports.runCQ = async ({ file, settings = {} } = {}) => {
|
||||
const tempFile = await makeFile(file, '.py', nanoid)
|
||||
const command = `cq-cli/cq-cli --codec stl --infile /tmp/${tempFile}/main.py --outfile /tmp/${tempFile}/output.stl`
|
||||
console.log('command', command)
|
||||
|
||||
try {
|
||||
const result = await runCommand(command, 30000)
|
||||
return { result, tempFile }
|
||||
} catch (error) {
|
||||
return { error, tempFile }
|
||||
}
|
||||
}
|
||||
60
app/api/src/docker/cadquery/runCQ.ts
Normal file
60
app/api/src/docker/cadquery/runCQ.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { writeFiles, runCommand } from '../common/utils'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { readFile } from 'fs/promises'
|
||||
|
||||
export const runCQ = async ({
|
||||
file,
|
||||
settings: { deflection = 0.3, parameters } = {},
|
||||
} = {}) => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.py' },
|
||||
{
|
||||
file: JSON.stringify(parameters),
|
||||
fileName: 'params.json',
|
||||
},
|
||||
],
|
||||
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
|
||||
)
|
||||
const fullPath = `/tmp/${tempFile}/output.gz`
|
||||
const stlPath = `/tmp/${tempFile}/output.stl`
|
||||
const customizerPath = `/tmp/${tempFile}/customizer.json`
|
||||
const command = [
|
||||
`/var/task/cq-cli/cq-cli.py`,
|
||||
`--codec stl`,
|
||||
`--infile /tmp/${tempFile}/main.py`,
|
||||
`--outfile ${stlPath}`,
|
||||
`--outputopts "deflection:${deflection};angularDeflection:${deflection};"`,
|
||||
`--params /tmp/${tempFile}/params.json`,
|
||||
`--getparams ${customizerPath}`,
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
let consoleMessage = ''
|
||||
try {
|
||||
consoleMessage = await runCommand(command, 30000)
|
||||
const params = JSON.parse(
|
||||
await readFile(customizerPath, { encoding: 'ascii' })
|
||||
)
|
||||
await writeFiles(
|
||||
[
|
||||
{
|
||||
file: JSON.stringify({
|
||||
customizerParams: params,
|
||||
consoleMessage,
|
||||
type: 'stl',
|
||||
}),
|
||||
fileName: 'metadata.json',
|
||||
},
|
||||
],
|
||||
tempFile
|
||||
)
|
||||
await runCommand(
|
||||
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
15000,
|
||||
true
|
||||
)
|
||||
return { consoleMessage, fullPath }
|
||||
} catch (error) {
|
||||
return { error: consoleMessage || error, fullPath }
|
||||
}
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
accepts@~1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
||||
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
|
||||
dependencies:
|
||||
mime-types "~2.1.24"
|
||||
negotiator "0.6.2"
|
||||
|
||||
array-flatten@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
|
||||
|
||||
body-parser@1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
||||
dependencies:
|
||||
bytes "3.1.0"
|
||||
content-type "~1.0.4"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
http-errors "1.7.2"
|
||||
iconv-lite "0.4.24"
|
||||
on-finished "~2.3.0"
|
||||
qs "6.7.0"
|
||||
raw-body "2.4.0"
|
||||
type-is "~1.6.17"
|
||||
|
||||
bytes@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
|
||||
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
|
||||
|
||||
content-disposition@0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
|
||||
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
|
||||
content-type@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||
|
||||
cookie@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
|
||||
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
|
||||
|
||||
cors@^2.8.5:
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||
dependencies:
|
||||
object-assign "^4"
|
||||
vary "^1"
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
depd@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||
|
||||
destroy@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
|
||||
etag@~1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
express@^4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
|
||||
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
|
||||
dependencies:
|
||||
accepts "~1.3.7"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.19.0"
|
||||
content-disposition "0.5.3"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.4.0"
|
||||
cookie-signature "1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
finalhandler "~1.1.2"
|
||||
fresh "0.5.2"
|
||||
merge-descriptors "1.0.1"
|
||||
methods "~1.1.2"
|
||||
on-finished "~2.3.0"
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "0.1.7"
|
||||
proxy-addr "~2.0.5"
|
||||
qs "6.7.0"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.1.2"
|
||||
send "0.17.1"
|
||||
serve-static "1.14.1"
|
||||
setprototypeof "1.1.1"
|
||||
statuses "~1.5.0"
|
||||
type-is "~1.6.18"
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
finalhandler@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
||||
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
on-finished "~2.3.0"
|
||||
parseurl "~1.3.3"
|
||||
statuses "~1.5.0"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
forwarded@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
||||
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
|
||||
|
||||
fresh@0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
|
||||
|
||||
http-errors@1.7.2:
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
|
||||
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.3"
|
||||
setprototypeof "1.1.1"
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
http-errors@~1.7.2:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
|
||||
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.4"
|
||||
setprototypeof "1.1.1"
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
inherits@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
inherits@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
|
||||
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
|
||||
|
||||
methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
||||
|
||||
mime-db@1.46.0:
|
||||
version "1.46.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
|
||||
integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
|
||||
|
||||
mime-types@~2.1.24:
|
||||
version "2.1.29"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2"
|
||||
integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
|
||||
dependencies:
|
||||
mime-db "1.46.0"
|
||||
|
||||
mime@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
ms@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||
|
||||
nanoid@^3.1.20:
|
||||
version "3.1.20"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
|
||||
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
|
||||
|
||||
negotiator@0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||
|
||||
object-assign@^4:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
on-finished@~2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
|
||||
path-to-regexp@0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||
|
||||
proxy-addr@~2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
|
||||
dependencies:
|
||||
forwarded "~0.1.2"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
qs@6.7.0:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||
|
||||
range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
||||
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
|
||||
dependencies:
|
||||
bytes "3.1.0"
|
||||
http-errors "1.7.2"
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
send@0.17.1:
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
|
||||
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
destroy "~1.0.4"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
fresh "0.5.2"
|
||||
http-errors "~1.7.2"
|
||||
mime "1.6.0"
|
||||
ms "2.1.1"
|
||||
on-finished "~2.3.0"
|
||||
range-parser "~1.2.1"
|
||||
statuses "~1.5.0"
|
||||
|
||||
serve-static@1.14.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
|
||||
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
|
||||
dependencies:
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.3"
|
||||
send "0.17.1"
|
||||
|
||||
setprototypeof@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
||||
|
||||
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||
|
||||
toidentifier@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||
|
||||
type-is@~1.6.17, type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||
dependencies:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
|
||||
|
||||
utils-merge@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||
|
||||
vary@^1, vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
||||
@@ -1,41 +0,0 @@
|
||||
const { exec } = require('child_process')
|
||||
const { promises } = require('fs')
|
||||
const { writeFile } = promises
|
||||
|
||||
async function makeFile(file, extension = '.scad', makeHash) {
|
||||
const tempFile = 'a' + makeHash() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
|
||||
console.log(`file to write: ${file}`)
|
||||
|
||||
await runCommand(`mkdir /tmp/${tempFile}`)
|
||||
await writeFile(`/tmp/${tempFile}/main${extension}`, file)
|
||||
return tempFile
|
||||
}
|
||||
|
||||
async function runCommand(command, timeout = 5000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(`error: ${error.message}`)
|
||||
console.log(`stderr: ${stderr}`)
|
||||
console.log(`stdout: ${stdout}`)
|
||||
reject(stdout || stderr) // it seems random if the message is in stdout or stderr, but not normally both
|
||||
return
|
||||
}
|
||||
if (stderr) {
|
||||
console.log(`stderr: ${stderr}`)
|
||||
resolve(stderr)
|
||||
return
|
||||
}
|
||||
console.log(`stdout: ${stdout}`)
|
||||
resolve(stdout)
|
||||
})
|
||||
setTimeout(() => {
|
||||
reject('timeout')
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runCommand,
|
||||
makeFile,
|
||||
}
|
||||
151
app/api/src/docker/common/utils.ts
Normal file
151
app/api/src/docker/common/utils.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
const { exec } = require('child_process')
|
||||
const { promises } = require('fs')
|
||||
const { writeFile } = promises
|
||||
const { createHash } = require('crypto')
|
||||
import { readFile } from 'fs/promises'
|
||||
|
||||
export async function writeFiles(
|
||||
files: { file: string; fileName: string }[] = [],
|
||||
tempFile: string
|
||||
): Promise<string> {
|
||||
console.log(`file to write: ${files.length}`)
|
||||
|
||||
try {
|
||||
await runCommand(`mkdir /tmp/${tempFile}`)
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
await Promise.all(
|
||||
files.map(({ file, fileName }) =>
|
||||
writeFile(`/tmp/${tempFile}/${fileName}`, file)
|
||||
)
|
||||
)
|
||||
return tempFile
|
||||
}
|
||||
|
||||
export async function runCommand(
|
||||
command,
|
||||
timeout = 5000,
|
||||
shouldRejectStdErr = false
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(`error: ${error.message}`)
|
||||
console.log(`stderr: ${stderr}`)
|
||||
console.log(`stdout: ${stdout}`)
|
||||
reject(stdout || stderr) // it seems random if the message is in stdout or stderr, but not normally both
|
||||
return
|
||||
}
|
||||
if (stderr) {
|
||||
console.log(`stderr: ${stderr}`)
|
||||
if (shouldRejectStdErr) {
|
||||
reject(stderr)
|
||||
return
|
||||
}
|
||||
resolve(stderr)
|
||||
return
|
||||
}
|
||||
console.log(`stdout: ${stdout}`)
|
||||
resolve(stdout)
|
||||
})
|
||||
setTimeout(() => {
|
||||
reject('timeout')
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
function makeHash(script) {
|
||||
return createHash('sha256').update(script).digest('hex')
|
||||
}
|
||||
|
||||
async function checkIfAlreadyExists(params, s3) {
|
||||
try {
|
||||
await s3.headObject(params).promise()
|
||||
return { isAlreadyInBucket: true }
|
||||
} catch (e) {
|
||||
console.log("couldn't find it", e)
|
||||
return { isAlreadyInBucket: false }
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectUrl(params, s3, tk) {
|
||||
const getTruncatedTime = () => {
|
||||
const currentTime = new Date()
|
||||
const d = new Date(currentTime)
|
||||
|
||||
d.setMinutes(Math.floor(d.getMinutes() / 10) * 10)
|
||||
d.setSeconds(0)
|
||||
d.setMilliseconds(0)
|
||||
|
||||
return d
|
||||
}
|
||||
const HALF_HOUR = 1800
|
||||
return tk.withFreeze(getTruncatedTime(), () =>
|
||||
s3.getSignedUrl('getObject', {
|
||||
...params,
|
||||
Expires: HALF_HOUR,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function loggerWrap(handler) {
|
||||
return (req, _context, callback) => {
|
||||
try {
|
||||
return handler(req, _context, callback)
|
||||
} catch (e) {
|
||||
console.log('error in handler', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
}: {
|
||||
error: string
|
||||
callback: Function
|
||||
fullPath: string
|
||||
consoleMessage: string
|
||||
}) {
|
||||
if (error) {
|
||||
const response = {
|
||||
statusCode: 400,
|
||||
body: Buffer.from(JSON.stringify({ error, fullPath })).toString('base64'),
|
||||
isBase64Encoded: true,
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
} else {
|
||||
console.log(`got result in route: ${consoleMessage}, file is: ${fullPath}`)
|
||||
let buffer = ''
|
||||
|
||||
try {
|
||||
buffer = await readFile(fullPath, { encoding: 'base64' })
|
||||
} catch (e) {
|
||||
console.log('read file error', e)
|
||||
const response = {
|
||||
statusCode: 400,
|
||||
body: Buffer.from(
|
||||
JSON.stringify({ error: consoleMessage, fullPath })
|
||||
).toString('base64'),
|
||||
isBase64Encoded: true,
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
}
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
body: buffer,
|
||||
isBase64Encoded: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/javascript',
|
||||
'Content-Encoding': 'gzip',
|
||||
},
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,47 @@
|
||||
services:
|
||||
# aws-emulator:
|
||||
# build: .
|
||||
# networks:
|
||||
# - awsland
|
||||
# ports:
|
||||
# - "5050:8080"
|
||||
|
||||
openscad-health:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./openscad/.
|
||||
image: openscad
|
||||
command: openscad.health
|
||||
ports:
|
||||
- "5051:8080"
|
||||
|
||||
openscad-preview:
|
||||
build:
|
||||
context: ../../../
|
||||
dockerfile: ./api/src/docker/openscad/Dockerfile
|
||||
image: openscad
|
||||
# build: ./openscad/.
|
||||
command: openscad.preview
|
||||
# networks:
|
||||
# - awsland
|
||||
# Adding volumes so that the containers can be restarted for js only changes in local dev
|
||||
volumes:
|
||||
- ../dist/docker/openscad:/var/task/js/
|
||||
- ../dist/docker/common:/var/task/common/
|
||||
ports:
|
||||
- "5052:8080"
|
||||
environment:
|
||||
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
|
||||
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
|
||||
BUCKET: "${DEV_BUCKET}"
|
||||
|
||||
openscad-stl:
|
||||
image: openscad
|
||||
# build: ./openscad/.
|
||||
volumes:
|
||||
- ../dist/docker/openscad:/var/task/js/
|
||||
- ../dist/docker/common:/var/task/common/
|
||||
command: openscad.stl
|
||||
ports:
|
||||
- "5053:8080"
|
||||
environment:
|
||||
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
|
||||
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
|
||||
BUCKET: "${DEV_BUCKET}"
|
||||
|
||||
cadquery-stl:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./cadquery/.
|
||||
context: ../../../
|
||||
dockerfile: ./api/src/docker/cadquery/Dockerfile
|
||||
volumes:
|
||||
- ../dist/docker/cadquery:/var/task/js/
|
||||
- ../dist/docker/common:/var/task/common/
|
||||
command: cadquery.stl
|
||||
ports:
|
||||
- 5060:8080
|
||||
environment:
|
||||
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
|
||||
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
|
||||
BUCKET: "${DEV_BUCKET}"
|
||||
|
||||
# networks:
|
||||
# awsland:
|
||||
# name: awsland
|
||||
|
||||
@@ -3,15 +3,18 @@ FROM public.ecr.aws/lts/ubuntu:20.04_stable
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
## install things needed to run openscad (xvfb is an important one)
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get update --fix-missing -qq
|
||||
# double check this below, I'm not sure we need inkscape etc
|
||||
RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb imagemagick unzip inkscape
|
||||
RUN apt-get install -y curl wget
|
||||
RUN touch /etc/apt/sources.list.d/openscad.list
|
||||
RUN echo "deb https://download.opensuse.org/repositories/home:/t-paul/xUbuntu_20.04/ ./" >> /etc/apt/sources.list.d/openscad.list
|
||||
RUN wget -qO - https://files.openscad.org/OBS-Repository-Key.pub | apt-key add -
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get install -y -qq openscad
|
||||
RUN apt-get install -y curl
|
||||
RUN apt-get install -y openscad-nightly
|
||||
|
||||
# install node14, see comment at the to of node14source_setup.sh
|
||||
ADD common/node14source_setup.sh /nodesource_setup.sh
|
||||
ADD api/src/docker/common/node14source_setup.sh /nodesource_setup.sh
|
||||
RUN ["chmod", "+x", "/nodesource_setup.sh"]
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs
|
||||
@@ -29,18 +32,35 @@ RUN apt-get update && \
|
||||
# Add the lambda emulator for local dev, (see entrypoint.sh for where it's used),
|
||||
# I have the file locally (gitignored) to speed up build times (as it downloads everytime),
|
||||
# but you can use the http version of the below ADD command or download it yourself from that url.
|
||||
ADD common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
ADD api/src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
# ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
RUN ["chmod", "+x", "/usr/local/bin/aws-lambda-rie"]
|
||||
|
||||
WORKDIR /var/task/
|
||||
COPY openscad/package*.json /var/task/
|
||||
RUN npm install
|
||||
# aws-lambda-ric does not play nice with yarn, so installing it seperately,
|
||||
# circle back to this later for a proper solution
|
||||
COPY package*.json /var/task/
|
||||
RUN npm install aws-lambda-ric@1.0.0
|
||||
|
||||
# Install OpenSCAD libraries
|
||||
# It's experimental, so only adding latest Round-Anything for now
|
||||
RUN echo "OPENSCADPATH=/var/task/openscad" >>/etc/profile && \
|
||||
wget -P /var/task/openscad/ https://github.com/Irev-Dev/Round-Anything/archive/refs/tags/1.0.4.zip && \
|
||||
unzip /var/task/openscad/1.0.4
|
||||
# Add our own theming (based on DeepOcean with a different "background" and "opencsg-face-back")
|
||||
COPY api/src/docker/openscad/cadhubtheme.json /usr/share/openscad-nightly/color-schemes/render/
|
||||
|
||||
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
|
||||
|
||||
# using built javascript from dist
|
||||
# run `yarn rw build` and $(npm bin)/zip-it-and-ship-it api/dist/functions/ api/dist/zipball before bulding this image
|
||||
COPY api/dist/zipball/openscad.zip /var/task/
|
||||
# -n stops aws-lamda-ric from being overridden.
|
||||
RUN unzip -n /var/task/openscad.zip
|
||||
|
||||
COPY api/src/docker/common/entrypoint.sh /entrypoint.sh
|
||||
|
||||
COPY openscad/*.js /var/task/
|
||||
COPY common/*.js /var/common/
|
||||
COPY common/entrypoint.sh /entrypoint.sh
|
||||
RUN ["chmod", "+x", "/entrypoint.sh"]
|
||||
|
||||
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||
CMD [ "openscad.render" ]
|
||||
CMD [ "openscad.preview" ]
|
||||
|
||||
19
app/api/src/docker/openscad/cadhubtheme.json
Normal file
19
app/api/src/docker/openscad/cadhubtheme.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name" : "CadHub",
|
||||
"index" : 1600,
|
||||
"show-in-gui" : true,
|
||||
|
||||
"colors" : {
|
||||
"background" : "#1A1A1D",
|
||||
"axes-color" : "#c1c1c1",
|
||||
"opencsg-face-front" : "#eeeeee",
|
||||
"opencsg-face-back" : "#8732F2",
|
||||
"cgal-face-front" : "#eeeeee",
|
||||
"cgal-face-back" : "#0babc8",
|
||||
"cgal-face-2d" : "#9370db",
|
||||
"cgal-edge-front" : "#0000ff",
|
||||
"cgal-edge-back" : "#0000ff",
|
||||
"cgal-edge-2d" : "#ff00ff",
|
||||
"crosshair" : "#f0f0f0"
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
const { runScad, stlExport } = require('./runScad')
|
||||
const middy = require('middy')
|
||||
const { cors } = require('middy/middlewares')
|
||||
|
||||
const health = async () => {
|
||||
console.log('Health endpoint')
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: 'ok',
|
||||
}
|
||||
}
|
||||
|
||||
// cors true does not seem to work in serverless.yml, perhaps docker lambdas aren't covered by that config
|
||||
// special lambda just for responding to options requests
|
||||
const preflightOptions = (req, _context, callback) => {
|
||||
const response = {
|
||||
statusCode: 204,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
},
|
||||
}
|
||||
callback(null, response)
|
||||
}
|
||||
|
||||
const preview = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
console.log(eventBody, 'eventBody')
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, result, tempFile } = await runScad({ file, settings })
|
||||
if (error) {
|
||||
const response = {
|
||||
statusCode: 400,
|
||||
body: JSON.stringify({ error, tempFile }),
|
||||
}
|
||||
callback(null, response)
|
||||
} else {
|
||||
console.log(`got result in route: ${result}, file is: ${tempFile}`)
|
||||
const fs = require('fs')
|
||||
const image = fs.readFileSync(`/tmp/${tempFile}/output.png`, {
|
||||
encoding: 'base64',
|
||||
})
|
||||
console.log(image, 'encoded image')
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
imageBase64: image,
|
||||
result,
|
||||
tempFile,
|
||||
}),
|
||||
}
|
||||
callback(null, response)
|
||||
}
|
||||
}
|
||||
|
||||
const stl = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
console.log(eventBody, 'eventBody')
|
||||
const { file } = JSON.parse(eventBody)
|
||||
const { error, result, tempFile } = await stlExport({ file })
|
||||
if (error) {
|
||||
const response = {
|
||||
statusCode: 400,
|
||||
body: { error, tempFile },
|
||||
}
|
||||
callback(null, response)
|
||||
} else {
|
||||
console.log(`got result in route: ${result}, file is: ${tempFile}`)
|
||||
const fs = require('fs')
|
||||
const stl = fs.readFileSync(`/tmp/${tempFile}/output.stl`, {
|
||||
encoding: 'base64',
|
||||
})
|
||||
console.log('encoded stl', stl)
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'content-type': 'application/stl',
|
||||
},
|
||||
body: stl,
|
||||
isBase64Encoded: true,
|
||||
}
|
||||
console.log('callback fired')
|
||||
callback(null, response)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
health: middy(health).use(cors()),
|
||||
stl: middy(stl).use(cors()),
|
||||
preview: middy(preview).use(cors()),
|
||||
preflightOptions,
|
||||
}
|
||||
44
app/api/src/docker/openscad/openscad.ts
Normal file
44
app/api/src/docker/openscad/openscad.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { runScad, stlExport } from './runScad'
|
||||
import middy from 'middy'
|
||||
import { cors } from 'middy/middlewares'
|
||||
import { loggerWrap, storeAssetAndReturnUrl } from '../common/utils'
|
||||
|
||||
const _preview = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
console.log('eventBody', eventBody)
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await runScad({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
})
|
||||
}
|
||||
|
||||
const _stl = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
|
||||
console.log(eventBody, 'eventBody')
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await stlExport({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
})
|
||||
}
|
||||
|
||||
export const stl = middy(loggerWrap(_stl)).use(cors())
|
||||
export const preview = middy(loggerWrap(_preview)).use(cors())
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "openscad-endpoint",
|
||||
"version": "0.0.1",
|
||||
"description": "endpoint for openscad",
|
||||
"main": "index.js",
|
||||
"author": "Kurt Hutten <kurt@kurthutten.com>",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"middy": "^0.36.0",
|
||||
"nanoid": "^3.1.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-lambda-ric": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
const { makeFile, runCommand } = require('../common/utils')
|
||||
const { nanoid } = require('nanoid')
|
||||
|
||||
module.exports.runScad = async ({
|
||||
file,
|
||||
settings: {
|
||||
size: { x = 500, y = 500 } = {},
|
||||
camera: {
|
||||
position = { x: 40, y: 40, z: 40 },
|
||||
rotation = { x: 55, y: 0, z: 25 },
|
||||
dist = 200,
|
||||
} = {},
|
||||
} = {}, // TODO add view settings
|
||||
} = {}) => {
|
||||
const tempFile = await makeFile(file, '.scad', nanoid)
|
||||
const { x: rx, y: ry, z: rz } = rotation
|
||||
const { x: px, y: py, z: pz } = position
|
||||
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
|
||||
const command = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad -o /tmp/${tempFile}/output.png ${cameraArg} --imgsize=${x},${y} --colorscheme DeepOcean /tmp/${tempFile}/main.scad`
|
||||
console.log('command', command)
|
||||
|
||||
try {
|
||||
const result = await runCommand(command, 15000)
|
||||
return { result, tempFile }
|
||||
} catch (error) {
|
||||
return { error, tempFile }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.stlExport = async ({ file } = {}) => {
|
||||
const tempFile = await makeFile(file, '.scad', nanoid)
|
||||
|
||||
try {
|
||||
const result = await runCommand(
|
||||
`openscad -o /tmp/${tempFile}/output.stl /tmp/${tempFile}/main.scad`,
|
||||
300000 // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
|
||||
)
|
||||
return { result, tempFile }
|
||||
} catch (error) {
|
||||
return { error, tempFile }
|
||||
}
|
||||
}
|
||||
150
app/api/src/docker/openscad/runScad.ts
Normal file
150
app/api/src/docker/openscad/runScad.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { writeFiles, runCommand } from '../common/utils'
|
||||
import { nanoid } from 'nanoid'
|
||||
const { readFile } = require('fs/promises')
|
||||
|
||||
const OPENSCAD_COMMON = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad-nightly`
|
||||
|
||||
/** Removes our generated/hash filename with just "main.scad", so that it's a nice message in the IDE */
|
||||
const cleanOpenScadError = (error) =>
|
||||
error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
|
||||
|
||||
export const runScad = async ({
|
||||
file,
|
||||
settings: {
|
||||
viewAll = false,
|
||||
size: { x = 500, y = 500 } = {},
|
||||
parameters,
|
||||
camera: {
|
||||
position = { x: 40, y: 40, z: 40 },
|
||||
rotation = { x: 55, y: 0, z: 25 },
|
||||
dist = 200,
|
||||
} = {},
|
||||
} = {}, // TODO add view settings
|
||||
} = {}): Promise<{
|
||||
error?: string
|
||||
consoleMessage?: string
|
||||
fullPath?: string
|
||||
customizerPath?: string
|
||||
}> => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.scad' },
|
||||
{
|
||||
file: JSON.stringify({
|
||||
parameterSets: { default: parameters },
|
||||
fileFormatVersion: '1',
|
||||
}),
|
||||
fileName: 'params.json',
|
||||
},
|
||||
],
|
||||
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
|
||||
)
|
||||
const { x: rx, y: ry, z: rz } = rotation
|
||||
const { x: px, y: py, z: pz } = position
|
||||
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
|
||||
const fullPath = `/tmp/${tempFile}/output.gz`
|
||||
const imPath = `/tmp/${tempFile}/output.png`
|
||||
const customizerPath = `/tmp/${tempFile}/customizer.param`
|
||||
const summaryPath = `/tmp/${tempFile}/summary.json` // contains camera info
|
||||
const command = [
|
||||
OPENSCAD_COMMON,
|
||||
`-o ${customizerPath}`,
|
||||
`-o ${imPath}`,
|
||||
`--summary camera --summary-file ${summaryPath}`,
|
||||
viewAll ? '--viewall' : '',
|
||||
`-p /tmp/${tempFile}/params.json -P default`,
|
||||
cameraArg,
|
||||
`--imgsize=${x},${y}`,
|
||||
`--colorscheme CadHub`,
|
||||
`/tmp/${tempFile}/main.scad`,
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
|
||||
try {
|
||||
const consoleMessage = await runCommand(command, 15000)
|
||||
const files: string[] = await Promise.all(
|
||||
[customizerPath, summaryPath].map((path) =>
|
||||
readFile(path, { encoding: 'ascii' })
|
||||
)
|
||||
)
|
||||
const [params, cameraInfo] = files.map((fileStr: string) =>
|
||||
JSON.parse(fileStr)
|
||||
)
|
||||
await writeFiles(
|
||||
[
|
||||
{
|
||||
file: JSON.stringify({
|
||||
cameraInfo: viewAll ? cameraInfo.camera : undefined,
|
||||
customizerParams: params.parameters,
|
||||
consoleMessage,
|
||||
type: 'png',
|
||||
}),
|
||||
fileName: 'metadata.json',
|
||||
},
|
||||
],
|
||||
tempFile
|
||||
)
|
||||
await runCommand(
|
||||
`cat ${imPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
15000
|
||||
)
|
||||
return { consoleMessage, fullPath, customizerPath }
|
||||
} catch (dirtyError) {
|
||||
return { error: cleanOpenScadError(dirtyError) }
|
||||
}
|
||||
}
|
||||
|
||||
export const stlExport = async ({ file, settings: { parameters } } = {}) => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.scad' },
|
||||
{
|
||||
file: JSON.stringify({
|
||||
parameterSets: { default: parameters },
|
||||
fileFormatVersion: '1',
|
||||
}),
|
||||
fileName: 'params.json',
|
||||
},
|
||||
],
|
||||
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
|
||||
)
|
||||
const fullPath = `/tmp/${tempFile}/output.gz`
|
||||
const stlPath = `/tmp/${tempFile}/output.stl`
|
||||
const customizerPath = `/tmp/${tempFile}/customizer.param`
|
||||
const command = [
|
||||
OPENSCAD_COMMON,
|
||||
// `--export-format=binstl`,
|
||||
`-o ${customizerPath}`,
|
||||
`-o ${stlPath}`,
|
||||
`-p /tmp/${tempFile}/params.json -P default`,
|
||||
`/tmp/${tempFile}/main.scad`,
|
||||
].join(' ')
|
||||
|
||||
try {
|
||||
// lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
|
||||
const consoleMessage = await runCommand(command, 60000)
|
||||
const params = JSON.parse(
|
||||
await readFile(customizerPath, { encoding: 'ascii' })
|
||||
).parameters
|
||||
await writeFiles(
|
||||
[
|
||||
{
|
||||
file: JSON.stringify({
|
||||
customizerParams: params,
|
||||
consoleMessage,
|
||||
type: 'stl',
|
||||
}),
|
||||
fileName: 'metadata.json',
|
||||
},
|
||||
],
|
||||
tempFile
|
||||
)
|
||||
await runCommand(
|
||||
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
15000
|
||||
)
|
||||
return { consoleMessage, fullPath, customizerPath }
|
||||
} catch (error) {
|
||||
return { error, fullPath }
|
||||
}
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
accepts@~1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
||||
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
|
||||
dependencies:
|
||||
mime-types "~2.1.24"
|
||||
negotiator "0.6.2"
|
||||
|
||||
array-flatten@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
|
||||
|
||||
body-parser@1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
||||
dependencies:
|
||||
bytes "3.1.0"
|
||||
content-type "~1.0.4"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
http-errors "1.7.2"
|
||||
iconv-lite "0.4.24"
|
||||
on-finished "~2.3.0"
|
||||
qs "6.7.0"
|
||||
raw-body "2.4.0"
|
||||
type-is "~1.6.17"
|
||||
|
||||
bytes@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
|
||||
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
|
||||
|
||||
content-disposition@0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
|
||||
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
|
||||
content-type@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||
|
||||
cookie@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
|
||||
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
|
||||
|
||||
cors@^2.8.5:
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||
dependencies:
|
||||
object-assign "^4"
|
||||
vary "^1"
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
depd@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||
|
||||
destroy@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
|
||||
etag@~1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
express@^4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
|
||||
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
|
||||
dependencies:
|
||||
accepts "~1.3.7"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.19.0"
|
||||
content-disposition "0.5.3"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.4.0"
|
||||
cookie-signature "1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
finalhandler "~1.1.2"
|
||||
fresh "0.5.2"
|
||||
merge-descriptors "1.0.1"
|
||||
methods "~1.1.2"
|
||||
on-finished "~2.3.0"
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "0.1.7"
|
||||
proxy-addr "~2.0.5"
|
||||
qs "6.7.0"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.1.2"
|
||||
send "0.17.1"
|
||||
serve-static "1.14.1"
|
||||
setprototypeof "1.1.1"
|
||||
statuses "~1.5.0"
|
||||
type-is "~1.6.18"
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
finalhandler@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
||||
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
on-finished "~2.3.0"
|
||||
parseurl "~1.3.3"
|
||||
statuses "~1.5.0"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
forwarded@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
||||
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
|
||||
|
||||
fresh@0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
|
||||
|
||||
http-errors@1.7.2:
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
|
||||
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.3"
|
||||
setprototypeof "1.1.1"
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
http-errors@~1.7.2:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
|
||||
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.4"
|
||||
setprototypeof "1.1.1"
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
inherits@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
inherits@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
|
||||
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
|
||||
|
||||
methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
||||
|
||||
mime-db@1.46.0:
|
||||
version "1.46.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
|
||||
integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
|
||||
|
||||
mime-types@~2.1.24:
|
||||
version "2.1.29"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2"
|
||||
integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
|
||||
dependencies:
|
||||
mime-db "1.46.0"
|
||||
|
||||
mime@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
ms@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||
|
||||
nanoid@^3.1.20:
|
||||
version "3.1.20"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
|
||||
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
|
||||
|
||||
negotiator@0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||
|
||||
object-assign@^4:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
on-finished@~2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
|
||||
path-to-regexp@0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||
|
||||
proxy-addr@~2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
|
||||
dependencies:
|
||||
forwarded "~0.1.2"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
qs@6.7.0:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||
|
||||
range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
||||
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
|
||||
dependencies:
|
||||
bytes "3.1.0"
|
||||
http-errors "1.7.2"
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
send@0.17.1:
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
|
||||
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
destroy "~1.0.4"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
fresh "0.5.2"
|
||||
http-errors "~1.7.2"
|
||||
mime "1.6.0"
|
||||
ms "2.1.1"
|
||||
on-finished "~2.3.0"
|
||||
range-parser "~1.2.1"
|
||||
statuses "~1.5.0"
|
||||
|
||||
serve-static@1.14.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
|
||||
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
|
||||
dependencies:
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.3"
|
||||
send "0.17.1"
|
||||
|
||||
setprototypeof@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
||||
|
||||
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||
|
||||
toidentifier@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||
|
||||
type-is@~1.6.17, type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||
dependencies:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
|
||||
|
||||
utils-merge@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||
|
||||
vary@^1, vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "aws-emulator",
|
||||
"version": "1.0.0",
|
||||
"description": "thin layer so that we can use docker lambdas locally",
|
||||
"scripts": {
|
||||
"lambdas": "docker-compose up --build",
|
||||
"emulate": "nodemon ./aws-emulator.js",
|
||||
"watch": "concurrently \"yarn lambdas\" \"yarn emulate\""
|
||||
},
|
||||
"main": "aws-emulator.js",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^6.0.0",
|
||||
"nodemon": "^2.0.7"
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
service: cad-lambdas
|
||||
# app and org for use with dashboard.serverless.com
|
||||
#app: your-app-name
|
||||
#org: your-org-name
|
||||
|
||||
# plugins:
|
||||
# - serverless-offline
|
||||
|
||||
# You can pin your service to only deploy with a specific Serverless version
|
||||
# Check out our docs for more details
|
||||
frameworkVersion: '2'
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
lambdaHashingVersion: 20201221
|
||||
ecr:
|
||||
images:
|
||||
# this image is built locally and push to ECR
|
||||
openscadimage:
|
||||
path: ./
|
||||
file: ./openscad/Dockerfile
|
||||
cadqueryimage:
|
||||
path: ./
|
||||
file: ./cadquery/Dockerfile
|
||||
apiGateway:
|
||||
metrics: true
|
||||
binaryMediaTypes:
|
||||
# we need to allow binary types to be able to send back images and stls, but it would be better to be more specific
|
||||
# ie image/png etc. as */* treats everything as binary including the json body as the input the lambdas
|
||||
# which mean we need to decode the input bode from base64, but the images break with anything other than */* :(
|
||||
- '*/*'
|
||||
|
||||
# you can overwrite defaults here
|
||||
# stage: dev
|
||||
# region: us-east-1
|
||||
|
||||
# you can add statements to the Lambda function's IAM Role here
|
||||
# iamRoleStatements:
|
||||
# - Effect: "Allow"
|
||||
# Action:
|
||||
# - "s3:ListBucket"
|
||||
# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] }
|
||||
# - Effect: "Allow"
|
||||
# Action:
|
||||
# - "s3:PutObject"
|
||||
# Resource:
|
||||
# Fn::Join:
|
||||
# - ""
|
||||
# - - "arn:aws:s3:::"
|
||||
# - "Ref" : "ServerlessDeploymentBucket"
|
||||
# - "/*"
|
||||
|
||||
# you can define service wide environment variables here
|
||||
# environment:
|
||||
# variable1: value1
|
||||
|
||||
functions:
|
||||
# see preflightoptions comment in openscad.js
|
||||
preflightopenscadpreview:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.preflightOptions
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: openscad/preview
|
||||
method: options
|
||||
preflightopenscadstl:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.preflightOptions
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: openscad/stl
|
||||
method: options
|
||||
openscadpreview:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.preview
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: openscad/preview
|
||||
method: post
|
||||
timeout: 15
|
||||
openscadstl:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.stl
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: openscad/stl
|
||||
method: post
|
||||
timeout: 30
|
||||
|
||||
preflightcadquerystl:
|
||||
image:
|
||||
name: cadqueryimage
|
||||
command:
|
||||
- cadquery.preflightOptions
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: cadquery/stl
|
||||
method: options
|
||||
cadquerystl:
|
||||
image:
|
||||
name: cadqueryimage
|
||||
command:
|
||||
- cadquery.stl
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: cadquery/stl
|
||||
method: post
|
||||
timeout: 30
|
||||
# The following are a few example events you can configure
|
||||
# NOTE: Please make sure to change your handler code to work with those events
|
||||
# Check the event documentation for details
|
||||
# events:
|
||||
# - httpApi:
|
||||
# path: /users/create
|
||||
# method: get
|
||||
# - websocket: $connect
|
||||
# - s3: ${env:BUCKET}
|
||||
# - schedule: rate(10 minutes)
|
||||
# - sns: greeter-topic
|
||||
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
|
||||
# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
|
||||
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
|
||||
# - iot:
|
||||
# sql: "SELECT * FROM 'some_topic'"
|
||||
# - cloudwatchEvent:
|
||||
# event:
|
||||
# source:
|
||||
# - "aws.ec2"
|
||||
# detail-type:
|
||||
# - "EC2 Instance State-change Notification"
|
||||
# detail:
|
||||
# state:
|
||||
# - pending
|
||||
# - cloudwatchLog: '/aws/lambda/hello'
|
||||
# - cognitoUserPool:
|
||||
# pool: MyUserPool
|
||||
# trigger: PreSignUp
|
||||
# - alb:
|
||||
# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/
|
||||
# priority: 1
|
||||
# conditions:
|
||||
# host: example.com
|
||||
# path: /hello
|
||||
|
||||
# Define function environment variables here
|
||||
# environment:
|
||||
# variable2: value2
|
||||
|
||||
# you can add CloudFormation resource templates here
|
||||
#resources:
|
||||
# Resources:
|
||||
# NewResource:
|
||||
# Type: AWS::S3::Bucket
|
||||
# Properties:
|
||||
# BucketName: my-new-bucket
|
||||
# Outputs:
|
||||
# NewOutput:
|
||||
# Description: "Description for the output"
|
||||
# Value: "Some output value"
|
||||
3
app/api/src/functions/cadquery.ts
Normal file
3
app/api/src/functions/cadquery.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { stl } from 'src/docker/cadquery/cadquery'
|
||||
|
||||
export { stl }
|
||||
40
app/api/src/functions/check-user-name/check-user-name.ts
Normal file
40
app/api/src/functions/check-user-name/check-user-name.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { APIGatewayEvent /*, Context*/ } from 'aws-lambda'
|
||||
import { logger } from 'src/lib/logger'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
/**
|
||||
* The handler function is your code that processes http request events.
|
||||
* You can use return and throw to send a response or error, respectively.
|
||||
*
|
||||
* Important: When deployed, a custom serverless function is an open API endpoint and
|
||||
* is your responsibility to secure appropriately.
|
||||
*
|
||||
* @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations}
|
||||
* in the RedwoodJS documentation for more information.
|
||||
*
|
||||
* @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent
|
||||
* @typedef { import('aws-lambda').Context } Context
|
||||
* @param { APIGatewayEvent } event - an object which contains information from the invoker.
|
||||
* @param { Context } context - contains information about the invocation,
|
||||
* function, and execution environment.
|
||||
*/
|
||||
export const handler = async (event: APIGatewayEvent /*context: Context*/) => {
|
||||
logger.info('Invoked checkUserName function')
|
||||
const userName = event.queryStringParameters.username
|
||||
let isUserNameAvailable = false
|
||||
try {
|
||||
const user = await db.user.findUnique({ where: { userName } })
|
||||
isUserNameAvailable = !user
|
||||
} catch (error) {
|
||||
isUserNameAvailable = false
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
isUserNameAvailable,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
createGraphQLHandler,
|
||||
makeMergedSchema,
|
||||
makeServices,
|
||||
} from '@redwoodjs/api'
|
||||
|
||||
import schemas from 'src/graphql/**/*.{js,ts}'
|
||||
import services from 'src/services/**/*.{js,ts}'
|
||||
|
||||
import { getCurrentUser } from 'src/lib/auth'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const handler = createGraphQLHandler({
|
||||
getCurrentUser,
|
||||
schema: makeMergedSchema({
|
||||
schemas,
|
||||
services: makeServices({ services }),
|
||||
}),
|
||||
onException: () => {
|
||||
// Disconnect from your database with an unhandled exception.
|
||||
db.$disconnect()
|
||||
},
|
||||
})
|
||||
28
app/api/src/functions/graphql.ts
Normal file
28
app/api/src/functions/graphql.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
|
||||
import { createSentryApolloPlugin } from 'src/lib/sentry'
|
||||
import { logger } from 'src/lib/logger'
|
||||
|
||||
import directives from 'src/directives/**/*.{js,ts}'
|
||||
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
|
||||
import services from 'src/services/**/*.{js,ts}'
|
||||
|
||||
import { getCurrentUser } from 'src/lib/auth'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const handler = createGraphQLHandler({
|
||||
loggerConfig: { logger, options: {} },
|
||||
getCurrentUser,
|
||||
directives,
|
||||
sdls,
|
||||
services,
|
||||
plugins: [createSentryApolloPlugin()],
|
||||
cors: {
|
||||
origin: '*',
|
||||
credentials: true,
|
||||
},
|
||||
|
||||
onException: () => {
|
||||
// Disconnect from your database with an unhandled exception.
|
||||
db.$disconnect()
|
||||
},
|
||||
})
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createUserInsecure } from 'src/services/users/users.js'
|
||||
import { createUserInsecure } from 'src/services/users/users'
|
||||
import { db } from 'src/lib/db'
|
||||
import { sentryWrapper } from 'src/lib/sentry'
|
||||
import { enforceAlphaNumeric, generateUniqueString } from 'src/services/helpers'
|
||||
import 'graphql-tag'
|
||||
import { sendMail } from 'src/lib/sendmail'
|
||||
|
||||
export const handler = async (req, _context) => {
|
||||
const unWrappedHandler = async (req, _context) => {
|
||||
const body = JSON.parse(req.body)
|
||||
console.log(body)
|
||||
console.log(_context)
|
||||
@@ -54,7 +57,7 @@ export const handler = async (req, _context) => {
|
||||
const user = body.user
|
||||
const email = user.email
|
||||
|
||||
let roles = []
|
||||
const roles = []
|
||||
|
||||
if (eventType === 'signup') {
|
||||
roles.push('user')
|
||||
@@ -64,13 +67,53 @@ export const handler = async (req, _context) => {
|
||||
})
|
||||
const userNameSeed = enforceAlphaNumeric(user?.user_metadata?.userName)
|
||||
const userName = await generateUniqueString(userNameSeed, isUniqueCallback) // TODO maybe come up with a better default userName?
|
||||
const name = user?.user_metadata?.full_name
|
||||
const input = {
|
||||
email,
|
||||
userName,
|
||||
name: user?.user_metadata?.full_name,
|
||||
name,
|
||||
id: user.id,
|
||||
}
|
||||
await createUserInsecure({ input })
|
||||
const kurtNotification = sendMail({
|
||||
to: 'k.hutten@protonmail.ch',
|
||||
from: {
|
||||
address: 'news@mail.cadhub.xyz',
|
||||
name: 'CadHub',
|
||||
},
|
||||
subject: `New Cadhub User`,
|
||||
text: JSON.stringify(input, null, 2),
|
||||
})
|
||||
const welcomeMsg = sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
address: 'news@mail.cadhub.xyz',
|
||||
name: 'CadHub',
|
||||
},
|
||||
subject: `${name} - Some things you should know about CadHub`,
|
||||
text: `Hi, My name's Kurt.
|
||||
|
||||
I started CadHub because I wanted a community hub for people who like CodeCAD as much of I do, you should know that the development of CadHub is very much a community effort as well and if you want get involved the discord is the best place to start https://discord.gg/SD7zFRNjGH.
|
||||
Long term I hope that CadHub will help push CodeCad as a paradigm forward, as there are clear benefits such as: CI/CD for parts, GIT based workflow and CodeCAD parts are normally much more robust to changes to parametric variables because the author can add logic to accommodate big changes where as GUI-CAD usually relies on blackbox heuristics and is more brittle. Sorry I'm getting into the weeds, if you want to read more on the paradigm see our blog https://learn.cadhub.xyz/.
|
||||
|
||||
One very easy way to help out is to give the repo a star (https://github.com/Irev-Dev/cadhub), or simply add any OpenSCAD or CadQuery models you have to the website, building out the library of parts atm is very important.
|
||||
|
||||
Hit me up anytime for questions or concerns.
|
||||
Cheers,
|
||||
Kurt.
|
||||
|
||||
k.hutten@protonmail.ch
|
||||
https://twitter.com/IrevDev
|
||||
irevdev#1888 - discord
|
||||
|
||||
`,
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.all([kurtNotification, welcomeMsg])
|
||||
} catch (e) {
|
||||
console.log('Problem sending emails', e)
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
@@ -82,3 +125,5 @@ export const handler = async (req, _context) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handler = sentryWrapper(unWrappedHandler)
|
||||
3
app/api/src/functions/openscad.ts
Normal file
3
app/api/src/functions/openscad.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { stl, preview } from 'src/docker/openscad/openscad'
|
||||
|
||||
export { stl, preview }
|
||||
41
app/api/src/graphql/ProjectReactions.sdl.js
Normal file
41
app/api/src/graphql/ProjectReactions.sdl.js
Normal file
@@ -0,0 +1,41 @@
|
||||
export const schema = gql`
|
||||
type ProjectReaction {
|
||||
id: String!
|
||||
emote: String!
|
||||
user: User!
|
||||
userId: String!
|
||||
project: Project!
|
||||
projectId: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
projectReactions: [ProjectReaction!]! @skipAuth
|
||||
projectReaction(id: String!): ProjectReaction @skipAuth
|
||||
projectReactionsByProjectId(projectId: String!): [ProjectReaction!]!
|
||||
@skipAuth
|
||||
}
|
||||
|
||||
input ToggleProjectReactionInput {
|
||||
emote: String!
|
||||
userId: String!
|
||||
projectId: String!
|
||||
}
|
||||
|
||||
input UpdateProjectReactionInput {
|
||||
emote: String
|
||||
userId: String
|
||||
projectId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
toggleProjectReaction(input: ToggleProjectReactionInput!): ProjectReaction!
|
||||
@requireAuth
|
||||
updateProjectReaction(
|
||||
id: String!
|
||||
input: UpdateProjectReactionInput!
|
||||
): ProjectReaction! @requireAuth
|
||||
deleteProjectReaction(id: String!): ProjectReaction! @requireAuth
|
||||
}
|
||||
`
|
||||
@@ -4,32 +4,33 @@ export const schema = gql`
|
||||
text: String!
|
||||
user: User!
|
||||
userId: String!
|
||||
part: Part!
|
||||
partId: String!
|
||||
project: Project!
|
||||
projectId: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
comments: [Comment!]!
|
||||
comment(id: String!): Comment
|
||||
comments: [Comment!]! @skipAuth
|
||||
comment(id: String!): Comment @skipAuth
|
||||
}
|
||||
|
||||
input CreateCommentInput {
|
||||
text: String!
|
||||
userId: String!
|
||||
partId: String!
|
||||
projectId: String!
|
||||
}
|
||||
|
||||
input UpdateCommentInput {
|
||||
text: String
|
||||
userId: String
|
||||
partId: String
|
||||
projectId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createComment(input: CreateCommentInput!): Comment!
|
||||
createComment(input: CreateCommentInput!): Comment! @requireAuth
|
||||
updateComment(id: String!, input: UpdateCommentInput!): Comment!
|
||||
deleteComment(id: String!): Comment!
|
||||
@requireAuth
|
||||
deleteComment(id: String!): Comment! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
20
app/api/src/graphql/email.sdl.ts
Normal file
20
app/api/src/graphql/email.sdl.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const schema = gql`
|
||||
type Envelope {
|
||||
from: String
|
||||
to: [String!]!
|
||||
}
|
||||
|
||||
type EmailResponse {
|
||||
accepted: [String!]!
|
||||
rejected: [String!]!
|
||||
}
|
||||
|
||||
input Email {
|
||||
subject: String!
|
||||
body: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
sendAllUsersEmail(input: Email!): EmailResponse! @requireAuth
|
||||
}
|
||||
`
|
||||
@@ -1,39 +0,0 @@
|
||||
export const schema = gql`
|
||||
type PartReaction {
|
||||
id: String!
|
||||
emote: String!
|
||||
user: User!
|
||||
userId: String!
|
||||
part: Part!
|
||||
partId: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
partReactions: [PartReaction!]!
|
||||
partReaction(id: String!): PartReaction
|
||||
partReactionsByPartId(partId: String!): [PartReaction!]!
|
||||
}
|
||||
|
||||
input TogglePartReactionInput {
|
||||
emote: String!
|
||||
userId: String!
|
||||
partId: String!
|
||||
}
|
||||
|
||||
input UpdatePartReactionInput {
|
||||
emote: String
|
||||
userId: String
|
||||
partId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
togglePartReaction(input: TogglePartReactionInput!): PartReaction!
|
||||
updatePartReaction(
|
||||
id: String!
|
||||
input: UpdatePartReactionInput!
|
||||
): PartReaction!
|
||||
deletePartReaction(id: String!): PartReaction!
|
||||
}
|
||||
`
|
||||
@@ -1,45 +0,0 @@
|
||||
export const schema = gql`
|
||||
type Part {
|
||||
id: String!
|
||||
title: String!
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deleted: Boolean!
|
||||
user: User!
|
||||
userId: String!
|
||||
Comment: [Comment]!
|
||||
Reaction(userId: String): [PartReaction]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
parts(userName: String): [Part!]!
|
||||
part(id: String!): Part
|
||||
partByUserAndTitle(userName: String!, partTitle: String!): Part
|
||||
}
|
||||
|
||||
input CreatePartInput {
|
||||
title: String!
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
userId: String!
|
||||
}
|
||||
|
||||
input UpdatePartInput {
|
||||
title: String
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
userId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createPart(input: CreatePartInput!): Part!
|
||||
forkPart(input: CreatePartInput!): Part!
|
||||
updatePart(id: String!, input: UpdatePartInput!): Part!
|
||||
deletePart(id: String!): Part!
|
||||
}
|
||||
`
|
||||
70
app/api/src/graphql/projects.sdl.ts
Normal file
70
app/api/src/graphql/projects.sdl.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export const schema = gql`
|
||||
type Project {
|
||||
id: String!
|
||||
title: String!
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
user: User!
|
||||
userId: String!
|
||||
deleted: Boolean!
|
||||
cadPackage: CadPackage!
|
||||
socialCard: SocialCard
|
||||
Comment: [Comment]!
|
||||
Reaction(userId: String): [ProjectReaction]!
|
||||
forkedFromId: String
|
||||
forkedFrom: Project
|
||||
childForks: [Project]!
|
||||
}
|
||||
|
||||
enum CadPackage {
|
||||
openscad
|
||||
cadquery
|
||||
jscad
|
||||
}
|
||||
|
||||
type Query {
|
||||
projects(userName: String): [Project!]! @skipAuth
|
||||
project(id: String!): Project @skipAuth
|
||||
projectByUserAndTitle(userName: String!, projectTitle: String!): Project
|
||||
@skipAuth
|
||||
}
|
||||
|
||||
input CreateProjectInput {
|
||||
title: String
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
userId: String!
|
||||
cadPackage: CadPackage!
|
||||
}
|
||||
|
||||
input ForkProjectInput {
|
||||
userId: String!
|
||||
forkedFromId: String
|
||||
code: String
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
title: String
|
||||
description: String
|
||||
code: String
|
||||
mainImage: String
|
||||
userId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createProject(input: CreateProjectInput!): Project! @requireAuth
|
||||
forkProject(input: ForkProjectInput!): Project! @requireAuth
|
||||
updateProject(id: String!, input: UpdateProjectInput!): Project!
|
||||
@requireAuth
|
||||
updateProjectImages(
|
||||
id: String!
|
||||
mainImage64: String
|
||||
socialCard64: String
|
||||
): Project! @requireAuth
|
||||
deleteProject(id: String!): Project! @requireAuth
|
||||
}
|
||||
`
|
||||
16
app/api/src/graphql/socialCards.sdl.ts
Normal file
16
app/api/src/graphql/socialCards.sdl.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const schema = gql`
|
||||
type SocialCard {
|
||||
id: String!
|
||||
projectId: String!
|
||||
project: Project!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
url: String
|
||||
outOfDate: Boolean!
|
||||
}
|
||||
|
||||
type Query {
|
||||
socialCards: [SocialCard!]! @skipAuth
|
||||
socialCard(id: String!): SocialCard @skipAuth
|
||||
}
|
||||
`
|
||||
@@ -10,8 +10,8 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Query {
|
||||
subjectAccessRequests: [SubjectAccessRequest!]!
|
||||
subjectAccessRequest(id: String!): SubjectAccessRequest
|
||||
subjectAccessRequests: [SubjectAccessRequest!]! @requireAuth
|
||||
subjectAccessRequest(id: String!): SubjectAccessRequest @requireAuth
|
||||
}
|
||||
|
||||
input CreateSubjectAccessRequestInput {
|
||||
@@ -29,11 +29,11 @@ export const schema = gql`
|
||||
type Mutation {
|
||||
createSubjectAccessRequest(
|
||||
input: CreateSubjectAccessRequestInput!
|
||||
): SubjectAccessRequest!
|
||||
): SubjectAccessRequest! @requireAuth
|
||||
updateSubjectAccessRequest(
|
||||
id: String!
|
||||
input: UpdateSubjectAccessRequestInput!
|
||||
): SubjectAccessRequest!
|
||||
deleteSubjectAccessRequest(id: String!): SubjectAccessRequest!
|
||||
): SubjectAccessRequest! @requireAuth
|
||||
deleteSubjectAccessRequest(id: String!): SubjectAccessRequest! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -8,17 +8,17 @@ export const schema = gql`
|
||||
updatedAt: DateTime!
|
||||
image: String
|
||||
bio: String
|
||||
Parts: [Part]!
|
||||
Part(partTitle: String): Part
|
||||
Reaction: [PartReaction]!
|
||||
Projects: [Project]!
|
||||
Project(projectTitle: String): Project
|
||||
Reaction: [ProjectReaction]!
|
||||
Comment: [Comment]!
|
||||
SubjectAccessRequest: [SubjectAccessRequest]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
users: [User!]!
|
||||
user(id: String!): User
|
||||
userName(userName: String!): User
|
||||
users: [User!]! @requireAuth
|
||||
user(id: String!): User @skipAuth
|
||||
userName(userName: String!): User @skipAuth
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
@@ -38,9 +38,10 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createUser(input: CreateUserInput!): User!
|
||||
updateUser(id: String!, input: UpdateUserInput!): User!
|
||||
createUser(input: CreateUserInput!): User! @requireAuth
|
||||
updateUser(id: String!, input: UpdateUserInput!): User! @requireAuth
|
||||
updateUserByUserName(userName: String!, input: UpdateUserInput!): User!
|
||||
deleteUser(id: String!): User!
|
||||
@requireAuth
|
||||
deleteUser(id: String!): User! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,61 +1,5 @@
|
||||
// Define what you want `currentUser` to return throughout your app. For example,
|
||||
// to return a real user from your database, you could do something like:
|
||||
//
|
||||
// export const getCurrentUser = async ({ email }) => {
|
||||
// return await db.user.findUnique({ where: { email } })
|
||||
// }
|
||||
//
|
||||
// If you want to enforce role-based access ...
|
||||
//
|
||||
// You'll need to set the currentUser's roles attributes to the
|
||||
// collection of roles as defined by your app.
|
||||
//
|
||||
// This allows requireAuth() on the api side and hasRole() in the useAuth() hook on the web side
|
||||
// to check if the user is assigned a given role or not.
|
||||
//
|
||||
// How you set the currentUser's roles depends on your auth provider and its implementation.
|
||||
//
|
||||
// For example, your decoded JWT may store `roles` in it namespaced `app_metadata`:
|
||||
//
|
||||
// {
|
||||
// 'https://example.com/app_metadata': { authorization: { roles: ['admin'] } },
|
||||
// 'https://example.com/user_metadata': {},
|
||||
// iss: 'https://app.us.auth0.com/',
|
||||
// sub: 'email|1234',
|
||||
// aud: [
|
||||
// 'https://example.com',
|
||||
// 'https://app.us.auth0.com/userinfo'
|
||||
// ],
|
||||
// iat: 1596481520,
|
||||
// exp: 1596567920,
|
||||
// azp: '1l0w6JXXXXL880T',
|
||||
// scope: 'openid profile email'
|
||||
// }
|
||||
//
|
||||
// The parseJWT utility will extract the roles from decoded token.
|
||||
//
|
||||
// The app_medata claim may or may not be namespaced based on the auth provider.
|
||||
// Note: Auth0 requires namespacing custom JWT claims
|
||||
//
|
||||
// Some providers, such as with Auth0, will set roles an authorization
|
||||
// attribute in app_metadata (namespaced or not):
|
||||
//
|
||||
// 'app_metadata': { authorization: { roles: ['publisher'] } }
|
||||
// 'https://example.com/app_metadata': { authorization: { roles: ['publisher'] } }
|
||||
//
|
||||
// Other providers may include roles simply within app_metadata:
|
||||
//
|
||||
// 'app_metadata': { roles: ['author'] }
|
||||
// 'https://example.com/app_metadata': { roles: ['author'] }
|
||||
//
|
||||
// And yet other may define roles as a custom claim at the root of the decoded token:
|
||||
//
|
||||
// roles: ['admin']
|
||||
//
|
||||
// The function `getCurrentUser` should return the user information
|
||||
// together with a collection of roles to check for role assignment:
|
||||
|
||||
import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
|
||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
|
||||
import { parseJWT } from '@redwoodjs/api'
|
||||
|
||||
/**
|
||||
* Use requireAuth in your services to check that a user is logged in,
|
||||
@@ -97,8 +41,24 @@ import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const getCurrentUser = async (decoded, { _token, _type }) => {
|
||||
return { ...decoded, roles: parseJWT({ decoded }).roles }
|
||||
export const getCurrentUser = async (
|
||||
decoded,
|
||||
{ _token, _type },
|
||||
{ _event, _context }
|
||||
) => {
|
||||
if (!decoded) {
|
||||
// if no decoded, then never set currentUser
|
||||
return null
|
||||
}
|
||||
|
||||
const { roles } = parseJWT({ decoded }) // extract and check roles separately
|
||||
|
||||
if (roles) {
|
||||
return { ...decoded, roles }
|
||||
}
|
||||
|
||||
return { ...decoded } // only return when certain you have
|
||||
// the currentUser properties
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +81,8 @@ export const getCurrentUser = async (decoded, { _token, _type }) => {
|
||||
* requireAuth({ role: ['editor', 'author'] })
|
||||
* requireAuth({ role: ['publisher'] })
|
||||
*/
|
||||
export const requireAuth = ({ role } = {}) => {
|
||||
export const requireAuth = ({ role }: { role?: string | string[] } = {}) => {
|
||||
console.log(context.currentUser)
|
||||
if (!context.currentUser) {
|
||||
throw new AuthenticationError("You don't have permission to do that.")
|
||||
}
|
||||
45
app/api/src/lib/cloudinary.ts
Normal file
45
app/api/src/lib/cloudinary.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
v2 as cloudinary,
|
||||
UploadApiResponse,
|
||||
UpdateApiOptions,
|
||||
} from 'cloudinary'
|
||||
|
||||
cloudinary.config({
|
||||
cloud_name: 'irevdev',
|
||||
api_key: process.env.CLOUDINARY_API_KEY,
|
||||
api_secret: process.env.CLOUDINARY_API_SECRET,
|
||||
})
|
||||
|
||||
interface UploadImageArgs {
|
||||
image64: string
|
||||
uploadPreset?: string
|
||||
publicId?: string
|
||||
invalidate: boolean
|
||||
}
|
||||
|
||||
export const uploadImage = async ({
|
||||
image64,
|
||||
uploadPreset = 'CadHub_project_images',
|
||||
publicId,
|
||||
invalidate = true,
|
||||
}: UploadImageArgs): Promise<UploadApiResponse> => {
|
||||
const options: UpdateApiOptions = { upload_preset: uploadPreset, invalidate }
|
||||
if (publicId) {
|
||||
options.public_id = publicId
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
cloudinary.uploader.upload(image64, options, (error, result) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const makeSocialPublicIdServer = (
|
||||
userName: string,
|
||||
projectTitle: string
|
||||
): string => `u-${userName}-slash-p-${projectTitle}`
|
||||
@@ -1,44 +0,0 @@
|
||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const requireOwnership = async ({ userId, userName, partId } = {}) => {
|
||||
// IMPORTANT, don't forget to await this function, as it will only block
|
||||
// unwanted db actions if it has time to look up resources in the db.
|
||||
if (!context.currentUser) {
|
||||
throw new AuthenticationError("You don't have permission to do that.")
|
||||
}
|
||||
if (!userId && !userName && !partId) {
|
||||
throw new ForbiddenError("You don't have access to do that.")
|
||||
}
|
||||
|
||||
if (context.currentUser.roles?.includes('admin')) {
|
||||
return
|
||||
}
|
||||
|
||||
const netlifyUserId = context.currentUser?.sub
|
||||
if (userId && userId !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
|
||||
if (userName) {
|
||||
const user = await db.user.findUnique({
|
||||
where: { userName },
|
||||
})
|
||||
|
||||
if (!user || user.id !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
}
|
||||
|
||||
if (partId) {
|
||||
const user = await db.part
|
||||
.findUnique({
|
||||
where: { id: partId },
|
||||
})
|
||||
.user()
|
||||
|
||||
if (!user || user.id !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
}
|
||||
}
|
||||
94
app/api/src/lib/owner.ts
Normal file
94
app/api/src/lib/owner.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
|
||||
import type { Project } from '@prisma/client'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const requireOwnership = async ({
|
||||
userId,
|
||||
userName,
|
||||
projectId,
|
||||
sub,
|
||||
}: {
|
||||
userId?: string
|
||||
userName?: string
|
||||
projectId?: string
|
||||
sub?: string
|
||||
} = {}) => {
|
||||
// IMPORTANT, don't forget to await this function, as it will only block
|
||||
// unwanted db actions if it has time to look up resources in the db.
|
||||
if (!(context?.currentUser || sub)) {
|
||||
throw new AuthenticationError("You don't have permission to do that.")
|
||||
}
|
||||
if (!userId && !userName && !projectId) {
|
||||
throw new ForbiddenError("You don't have access to do that.")
|
||||
}
|
||||
|
||||
if (context.currentUser.roles?.includes('admin')) {
|
||||
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
|
||||
throw new ForbiddenError("That's a local admin ONLY.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const netlifyUserId = context?.currentUser?.sub || sub
|
||||
if (userId && userId !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
|
||||
if (userName) {
|
||||
const user = await db.user.findUnique({
|
||||
where: { userName },
|
||||
})
|
||||
|
||||
if (!user || user.id !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
}
|
||||
|
||||
if (projectId) {
|
||||
const user = await db.project
|
||||
.findUnique({
|
||||
where: { id: projectId },
|
||||
})
|
||||
.user()
|
||||
|
||||
if (!user || user.id !== netlifyUserId) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
}
|
||||
}
|
||||
export const requireProjectOwnership = async ({
|
||||
projectId,
|
||||
}: {
|
||||
userId?: string
|
||||
userName?: string
|
||||
projectId?: string
|
||||
sub?: string
|
||||
} = {}): Promise<Project> => {
|
||||
// IMPORTANT, don't forget to await this function, as it will only block
|
||||
// unwanted db actions if it has time to look up resources in the db.
|
||||
if (!context?.currentUser) {
|
||||
throw new AuthenticationError("You don't have permission to do that.")
|
||||
}
|
||||
if (!projectId) {
|
||||
throw new ForbiddenError("You don't have access to do that.")
|
||||
}
|
||||
|
||||
const netlifyUserId = context?.currentUser?.sub
|
||||
|
||||
if (projectId || context.currentUser.roles?.includes('admin')) {
|
||||
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
|
||||
throw new ForbiddenError("That's a local admin ONLY.")
|
||||
}
|
||||
const project = await db.project.findUnique({
|
||||
where: { id: projectId },
|
||||
})
|
||||
const hasPermission =
|
||||
(project && project?.userId === netlifyUserId) ||
|
||||
context.currentUser.roles?.includes('admin')
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenError("You don't own this resource.")
|
||||
}
|
||||
return project
|
||||
}
|
||||
}
|
||||
63
app/api/src/lib/sendmail.ts
Normal file
63
app/api/src/lib/sendmail.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import nodemailer, { SendMailOptions } from 'nodemailer'
|
||||
|
||||
export interface SendMailArgs {
|
||||
to: string
|
||||
from: SendMailOptions['from']
|
||||
subject: string
|
||||
text: string
|
||||
}
|
||||
|
||||
interface SuccessResult {
|
||||
accepted: string[]
|
||||
rejected: string[]
|
||||
envelopeTime: number
|
||||
messageTime: number
|
||||
messageSize: number
|
||||
response: string
|
||||
envelope: {
|
||||
from: string | false
|
||||
to: string[]
|
||||
}
|
||||
messageId: string
|
||||
}
|
||||
|
||||
export function sendMail({
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
}: SendMailArgs): Promise<SuccessResult> {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: 'smtp.mailgun.org',
|
||||
port: 587,
|
||||
secure: false,
|
||||
tls: {
|
||||
ciphers: 'SSLv3',
|
||||
},
|
||||
auth: {
|
||||
user: 'postmaster@mail.cadhub.xyz',
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
console.log({ to, from, subject, text })
|
||||
|
||||
const emailPromise = new Promise((resolve, reject) => {
|
||||
transporter.sendMail(
|
||||
{
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(info)
|
||||
}
|
||||
}
|
||||
)
|
||||
}) as any as Promise<SuccessResult>
|
||||
return emailPromise
|
||||
}
|
||||
105
app/api/src/lib/sentry.ts
Normal file
105
app/api/src/lib/sentry.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Config, ApolloError } from '@redwoodjs/graphql-server'
|
||||
import * as Sentry from '@sentry/node'
|
||||
|
||||
let sentryInitialized = false
|
||||
if (process.env.SENTRY_DSN && !sentryInitialized) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
environment: process.env.CONTEXT,
|
||||
release: process.env.COMMIT_REF,
|
||||
})
|
||||
sentryInitialized = true
|
||||
}
|
||||
|
||||
async function reportError(error) {
|
||||
if (!sentryInitialized) return
|
||||
// If you do have authentication set up, we can add
|
||||
// some user data to help debug issues
|
||||
// if (context.currentUser) {
|
||||
// Sentry.configureScope((scope) => {
|
||||
// scope.setUser({
|
||||
// id: context?.currentUser?.id,
|
||||
// email: context?.currentUser?.email,
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
if (typeof error === 'string') {
|
||||
Sentry.captureMessage(error)
|
||||
} else {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
await Sentry.flush()
|
||||
}
|
||||
|
||||
export const sentryWrapper = (handler) => async (event, lambdaContext) => {
|
||||
lambdaContext.callbackWaitsForEmptyEventLoop = false
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const callback = (err, result) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
const resp = handler(event, lambdaContext, callback)
|
||||
if (resp?.then) {
|
||||
resp.then(resolve, reject)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
// This catches both sync errors & promise
|
||||
// rejections, because we 'await' on the handler
|
||||
await reportError(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export const createSentryApolloPlugin: Config['plugins'][number] = () => ({
|
||||
requestDidStart: () => {
|
||||
return {
|
||||
didEncounterErrors(ctx) {
|
||||
// If we couldn't parse the operation, don't
|
||||
// do anything here
|
||||
if (!ctx.operation) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const err of ctx.errors) {
|
||||
// Only report internal server errors,
|
||||
// all errors extending ApolloError should be user-facing
|
||||
if (err instanceof ApolloError) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add scoped report details and send to Sentry
|
||||
Sentry.withScope((scope) => {
|
||||
// Annotate whether failing operation was query/mutation/subscription
|
||||
scope.setTag('kind', ctx.operation.operation)
|
||||
|
||||
// Log query and variables as extras (make sure to strip out sensitive data!)
|
||||
scope.setExtra('query', ctx.request.query)
|
||||
scope.setExtra('variables', ctx.request.variables)
|
||||
|
||||
if (err.path) {
|
||||
// We can also add the path as breadcrumb
|
||||
scope.addBreadcrumb({
|
||||
category: 'query-path',
|
||||
message: err.path.join(' > '),
|
||||
level: Sentry.Severity.Debug,
|
||||
})
|
||||
}
|
||||
|
||||
const transactionId =
|
||||
ctx.request.http.headers.get('x-transaction-id')
|
||||
if (transactionId) {
|
||||
scope.setTransaction(transactionId)
|
||||
}
|
||||
|
||||
Sentry.captureException(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
import { comments } from './comments'
|
||||
*/
|
||||
|
||||
describe('comments', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -33,6 +33,6 @@ export const deleteComment = ({ id }) => {
|
||||
export const Comment = {
|
||||
user: (_obj, { root }) =>
|
||||
db.comment.findUnique({ where: { id: root.id } }).user(),
|
||||
part: (_obj, { root }) =>
|
||||
db.comment.findUnique({ where: { id: root.id } }).part(),
|
||||
project: (_obj, { root }) =>
|
||||
db.comment.findUnique({ where: { id: root.id } }).project(),
|
||||
}
|
||||
45
app/api/src/services/email/email.ts
Normal file
45
app/api/src/services/email/email.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { sendMail } from 'src/lib/sendmail'
|
||||
import type { SendMailArgs } from 'src/lib/sendmail'
|
||||
import { users } from 'src/services/users/users'
|
||||
|
||||
export const sendAllUsersEmail = async ({ input: { body, subject } }) => {
|
||||
requireAuth({ role: 'admin' })
|
||||
const from = {
|
||||
address: 'news@mail.cadhub.xyz',
|
||||
name: 'CadHub',
|
||||
}
|
||||
const emails: SendMailArgs[] = (await users()).map(({ email }) => ({
|
||||
to: email,
|
||||
from,
|
||||
subject,
|
||||
text: body,
|
||||
}))
|
||||
const emailPromises = emails.map((email) => sendMail(email))
|
||||
const accepted = []
|
||||
const rejected = []
|
||||
const result = await Promise.allSettled(emailPromises)
|
||||
result.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
accepted.push(result.value.accepted[0])
|
||||
} else {
|
||||
rejected.push(result.reason)
|
||||
}
|
||||
})
|
||||
await sendMail({
|
||||
to: 'k.hutten@protonmail.ch',
|
||||
from,
|
||||
subject: `All users email report`,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
accepted,
|
||||
rejected,
|
||||
originalEmailList: emails,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
})
|
||||
|
||||
return { accepted, rejected }
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { v2 as cloudinary } from 'cloudinary'
|
||||
import humanId from 'human-id'
|
||||
|
||||
cloudinary.config({
|
||||
cloud_name: 'irevdev',
|
||||
api_key: process.env.CLOUDINARY_API_KEY,
|
||||
@@ -20,7 +22,7 @@ export const foreignKeyReplacement = (input) => {
|
||||
}
|
||||
|
||||
export const enforceAlphaNumeric = (string) =>
|
||||
string.replace(/([^a-zA-Z\d_:])/g, '-')
|
||||
(string || '').replace(/([^a-zA-Z\d_:])/g, '-')
|
||||
|
||||
export const generateUniqueString = async (
|
||||
seed,
|
||||
@@ -36,6 +38,26 @@ export const generateUniqueString = async (
|
||||
return generateUniqueString(newSeed, isUniqueCallback, count)
|
||||
}
|
||||
|
||||
export const generateUniqueStringWithoutSeed = async (
|
||||
isUniqueCallback: (seed: string) => Promise<boolean>,
|
||||
count = 0
|
||||
) => {
|
||||
const seed = humanId({
|
||||
separator: '-',
|
||||
capitalize: false,
|
||||
})
|
||||
const isUnique = !(await isUniqueCallback(seed))
|
||||
if (isUnique) {
|
||||
return seed
|
||||
}
|
||||
count += 1
|
||||
if (count > 100) {
|
||||
console.log('trouble finding unique')
|
||||
return `very-unique-${seed}`.slice(0, 10)
|
||||
}
|
||||
return generateUniqueStringWithoutSeed(isUniqueCallback, count)
|
||||
}
|
||||
|
||||
export const destroyImage = ({ publicId }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
cloudinary.uploader.destroy(publicId, (error, result) => {
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
import { partReactions } from './partReactions'
|
||||
*/
|
||||
|
||||
describe('partReactions', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,113 +0,0 @@
|
||||
import { db } from 'src/lib/db'
|
||||
import {
|
||||
foreignKeyReplacement,
|
||||
enforceAlphaNumeric,
|
||||
generateUniqueString,
|
||||
destroyImage,
|
||||
} from 'src/services/helpers'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
|
||||
export const parts = ({ userName }) => {
|
||||
if (!userName) {
|
||||
return db.part.findMany({ where: { deleted: false } })
|
||||
}
|
||||
return db.part.findMany({
|
||||
where: {
|
||||
deleted: false,
|
||||
user: {
|
||||
userName,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const part = ({ id }) => {
|
||||
return db.part.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
export const partByUserAndTitle = async ({ userName, partTitle }) => {
|
||||
const user = await db.user.findUnique({
|
||||
where: {
|
||||
userName,
|
||||
},
|
||||
})
|
||||
return db.part.findUnique({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: partTitle,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createPart = async ({ input }) => {
|
||||
requireAuth()
|
||||
return db.part.create({
|
||||
data: foreignKeyReplacement(input),
|
||||
})
|
||||
}
|
||||
|
||||
export const forkPart = async ({ input }) => {
|
||||
// Only difference between create and fork part is that fork part will generate a unique title
|
||||
// (for the user) if there is a conflict
|
||||
const isUniqueCallback = async (seed) =>
|
||||
db.part.findUnique({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: seed,
|
||||
userId: input.userId,
|
||||
},
|
||||
},
|
||||
})
|
||||
const title = await generateUniqueString(input.title, isUniqueCallback)
|
||||
// TODO change the description to `forked from userName/partName ${rest of description}`
|
||||
return db.part.create({
|
||||
data: foreignKeyReplacement({ ...input, title }),
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePart = async ({ id, input }) => {
|
||||
requireAuth()
|
||||
await requireOwnership({ partId: id })
|
||||
if (input.title) {
|
||||
input.title = enforceAlphaNumeric(input.title)
|
||||
}
|
||||
const originalPart = await db.part.findUnique({ where: { id } })
|
||||
const imageToDestroy =
|
||||
originalPart.mainImage !== input.mainImage && originalPart.mainImage
|
||||
const update = await db.part.update({
|
||||
data: foreignKeyReplacement(input),
|
||||
where: { id },
|
||||
})
|
||||
if (imageToDestroy) {
|
||||
console.log(`image destroyed, publicId: ${imageToDestroy}, partId: ${id}`)
|
||||
// destroy after the db has been updated
|
||||
destroyImage({ publicId: imageToDestroy })
|
||||
}
|
||||
return update
|
||||
}
|
||||
|
||||
export const deletePart = async ({ id }) => {
|
||||
requireAuth()
|
||||
await requireOwnership({ partId: id })
|
||||
return db.part.update({
|
||||
data: {
|
||||
deleted: true,
|
||||
},
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const Part = {
|
||||
user: (_obj, { root }) =>
|
||||
db.part.findUnique({ where: { id: root.id } }).user(),
|
||||
Comment: (_obj, { root }) =>
|
||||
db.part.findUnique({ where: { id: root.id } }).Comment(),
|
||||
Reaction: (_obj, { root }) =>
|
||||
db.part
|
||||
.findUnique({ where: { id: root.id } })
|
||||
.Reaction({ where: { userId: _obj.userId } }),
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
import { parts } from './parts'
|
||||
*/
|
||||
|
||||
describe('parts', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,28 +1,28 @@
|
||||
import { UserInputError } from '@redwoodjs/api'
|
||||
import { UserInputError } from '@redwoodjs/graphql-server'
|
||||
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
import { db } from 'src/lib/db'
|
||||
import { foreignKeyReplacement } from 'src/services/helpers'
|
||||
|
||||
export const partReactions = () => {
|
||||
return db.partReaction.findMany()
|
||||
export const projectReactions = () => {
|
||||
return db.projectReaction.findMany()
|
||||
}
|
||||
|
||||
export const partReaction = ({ id }) => {
|
||||
return db.partReaction.findUnique({
|
||||
export const projectReaction = ({ id }) => {
|
||||
return db.projectReaction.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const partReactionsByPartId = ({ partId }) => {
|
||||
return db.partReaction.findMany({
|
||||
where: { partId: partId },
|
||||
export const projectReactionsByProjectId = ({ projectId }) => {
|
||||
return db.projectReaction.findMany({
|
||||
where: { projectId },
|
||||
})
|
||||
}
|
||||
|
||||
export const togglePartReaction = async ({ input }) => {
|
||||
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
||||
export const toggleProjectReaction = async ({ input }) => {
|
||||
// if write fails emote_userId_projectId @@unique constraint, then delete it instead
|
||||
requireAuth()
|
||||
await requireOwnership({ userId: input?.userId })
|
||||
const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js
|
||||
@@ -36,33 +36,33 @@ export const togglePartReaction = async ({ input }) => {
|
||||
let dbPromise
|
||||
const inputClone = { ...input } // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
|
||||
try {
|
||||
dbPromise = await db.partReaction.create({
|
||||
dbPromise = await db.projectReaction.create({
|
||||
data: foreignKeyReplacement(input),
|
||||
})
|
||||
} catch (e) {
|
||||
dbPromise = db.partReaction.delete({
|
||||
where: { emote_userId_partId: inputClone },
|
||||
dbPromise = db.projectReaction.delete({
|
||||
where: { emote_userId_projectId: inputClone },
|
||||
})
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
|
||||
export const updatePartReaction = ({ id, input }) => {
|
||||
return db.partReaction.update({
|
||||
export const updateProjectReaction = ({ id, input }) => {
|
||||
return db.projectReaction.update({
|
||||
data: foreignKeyReplacement(input),
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deletePartReaction = ({ id }) => {
|
||||
return db.partReaction.delete({
|
||||
export const deleteProjectReaction = ({ id }) => {
|
||||
return db.projectReaction.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const PartReaction = {
|
||||
export const ProjectReaction = {
|
||||
user: (_obj, { root }) =>
|
||||
db.partReaction.findUnique({ where: { id: root.id } }).user(),
|
||||
part: (_obj, { root }) =>
|
||||
db.partReaction.findUnique({ where: { id: root.id } }).part(),
|
||||
db.projectReaction.findUnique({ where: { id: root.id } }).user(),
|
||||
project: (_obj, { root }) =>
|
||||
db.projectReaction.findUnique({ where: { id: root.id } }).project(),
|
||||
}
|
||||
294
app/api/src/services/projects/projects.ts
Normal file
294
app/api/src/services/projects/projects.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { ResolverArgs } from '@redwoodjs/graphql-server'
|
||||
import type { Prisma, Project as ProjectType } from '@prisma/client'
|
||||
import { uploadImage, makeSocialPublicIdServer } from 'src/lib/cloudinary'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
import {
|
||||
foreignKeyReplacement,
|
||||
enforceAlphaNumeric,
|
||||
generateUniqueString,
|
||||
generateUniqueStringWithoutSeed,
|
||||
destroyImage,
|
||||
} from 'src/services/helpers'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
|
||||
|
||||
export const projects = ({ userName }) => {
|
||||
if (!userName) {
|
||||
return db.project.findMany({ where: { deleted: false } })
|
||||
}
|
||||
return db.project.findMany({
|
||||
where: {
|
||||
deleted: false,
|
||||
user: {
|
||||
userName,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const project = ({ id }: Prisma.ProjectWhereUniqueInput) => {
|
||||
return db.project.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
export const projectByUserAndTitle = async ({ userName, projectTitle }) => {
|
||||
const user = await db.user.findUnique({
|
||||
where: {
|
||||
userName,
|
||||
},
|
||||
})
|
||||
return db.project.findUnique({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: projectTitle,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
const isUniqueProjectTitle =
|
||||
(userId: string) =>
|
||||
async (seed: string): Promise<boolean> =>
|
||||
!!(await db.project.findUnique({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: seed,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
interface CreateProjectArgs {
|
||||
input: Prisma.ProjectCreateArgs['data']
|
||||
}
|
||||
|
||||
export const createProject = async ({ input }: CreateProjectArgs) => {
|
||||
requireAuth()
|
||||
console.log(input.userId)
|
||||
const isUniqueCallback = isUniqueProjectTitle(input.userId)
|
||||
let title = input.title
|
||||
if (!title) {
|
||||
title = await generateUniqueStringWithoutSeed(isUniqueCallback)
|
||||
}
|
||||
return db.project.create({
|
||||
data: foreignKeyReplacement({
|
||||
...input,
|
||||
title,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const forkProject = async ({ input }) => {
|
||||
requireAuth()
|
||||
const projectData = await db.project.findUnique({
|
||||
where: {
|
||||
id: input.forkedFromId,
|
||||
},
|
||||
})
|
||||
const isUniqueCallback = isUniqueProjectTitle(input.userId)
|
||||
let title = projectData.title
|
||||
|
||||
title = await generateUniqueString(title, isUniqueCallback)
|
||||
|
||||
const { code, description, cadPackage } = projectData
|
||||
|
||||
return db.project.create({
|
||||
data: foreignKeyReplacement({
|
||||
...input,
|
||||
title,
|
||||
code: input.code || code,
|
||||
description,
|
||||
cadPackage,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
interface UpdateProjectArgs extends Prisma.ProjectWhereUniqueInput {
|
||||
input: Prisma.ProjectUpdateInput
|
||||
}
|
||||
|
||||
export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
|
||||
const checkSocialCardValidity = async (
|
||||
projectId: string,
|
||||
input: UpdateProjectArgs['input'],
|
||||
oldProject: ProjectType
|
||||
) => {
|
||||
const titleChange = input.title && input.title !== oldProject.title
|
||||
const descriptionChange =
|
||||
input.description && input.description !== oldProject.description
|
||||
if (titleChange || descriptionChange) {
|
||||
const socialCard = await db.socialCard.findUnique({
|
||||
where: { projectId },
|
||||
})
|
||||
if (socialCard) {
|
||||
return db.socialCard.update({
|
||||
data: { outOfDate: true },
|
||||
where: { id: socialCard.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
requireAuth()
|
||||
const originalProject = await requireProjectOwnership({ projectId: id })
|
||||
if (input.title) {
|
||||
input.title = enforceAlphaNumeric(input.title)
|
||||
}
|
||||
const socialCardPromise = checkSocialCardValidity(id, input, originalProject)
|
||||
const imageToDestroy =
|
||||
originalProject.mainImage !== input.mainImage &&
|
||||
input.mainImage &&
|
||||
originalProject.mainImage
|
||||
const update = await db.project.update({
|
||||
data: foreignKeyReplacement(input),
|
||||
where: { id },
|
||||
})
|
||||
if (imageToDestroy) {
|
||||
console.log(
|
||||
`image destroyed, publicId: ${imageToDestroy}, projectId: ${id}, replacing image is ${input.mainImage}`
|
||||
)
|
||||
// destroy after the db has been updated
|
||||
await destroyImage({ publicId: imageToDestroy })
|
||||
}
|
||||
await socialCardPromise
|
||||
return update
|
||||
}
|
||||
|
||||
export const updateProjectImages = async ({
|
||||
id,
|
||||
mainImage64,
|
||||
socialCard64,
|
||||
}: {
|
||||
id: string
|
||||
mainImage64?: string
|
||||
socialCard64?: string
|
||||
}): Promise<ProjectType> => {
|
||||
requireAuth()
|
||||
const project = await requireProjectOwnership({ projectId: id })
|
||||
const replaceSocialCard = async () => {
|
||||
if (!socialCard64) {
|
||||
return
|
||||
}
|
||||
let publicId = ''
|
||||
let socialCardId = ''
|
||||
try {
|
||||
;({ id: socialCardId, url: publicId } = await db.socialCard.findUnique({
|
||||
where: { projectId: id },
|
||||
}))
|
||||
} catch (e) {
|
||||
const { userName } = await db.user.findUnique({
|
||||
where: { id: project.userId },
|
||||
})
|
||||
publicId = makeSocialPublicIdServer(userName, project.title)
|
||||
}
|
||||
const imagePromise = uploadImage({
|
||||
image64: socialCard64,
|
||||
uploadPreset: 'CadHub_project_images',
|
||||
publicId,
|
||||
invalidate: true,
|
||||
})
|
||||
const saveOrUpdateSocialCard = () => {
|
||||
const data = {
|
||||
outOfDate: false,
|
||||
url: publicId,
|
||||
}
|
||||
if (socialCardId) {
|
||||
return db.socialCard.update({
|
||||
data,
|
||||
where: { projectId: id },
|
||||
})
|
||||
}
|
||||
return db.socialCard.create({
|
||||
data: {
|
||||
...data,
|
||||
project: {
|
||||
connect: {
|
||||
id: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
const socialCardUpdatePromise = saveOrUpdateSocialCard()
|
||||
const [socialCard] = await Promise.all([
|
||||
socialCardUpdatePromise,
|
||||
imagePromise,
|
||||
])
|
||||
return socialCard
|
||||
}
|
||||
|
||||
const updateMainImage = async (): Promise<ProjectType> => {
|
||||
if (!mainImage64) {
|
||||
return project
|
||||
}
|
||||
const { public_id: mainImage } = await uploadImage({
|
||||
image64: mainImage64,
|
||||
uploadPreset: 'CadHub_project_images',
|
||||
invalidate: true,
|
||||
})
|
||||
const projectPromise = db.project.update({
|
||||
data: {
|
||||
mainImage,
|
||||
},
|
||||
where: { id },
|
||||
})
|
||||
let imageDestroyPromise = new Promise((r) => r(null))
|
||||
if (project.mainImage) {
|
||||
console.log(
|
||||
`image destroyed, publicId: ${project.mainImage}, projectId: ${id}, replacing image is ${mainImage}`
|
||||
)
|
||||
// destroy after the db has been updated
|
||||
imageDestroyPromise = destroyImage({ publicId: project.mainImage })
|
||||
}
|
||||
const [updatedProject] = await Promise.all([
|
||||
projectPromise,
|
||||
imageDestroyPromise,
|
||||
])
|
||||
return updatedProject
|
||||
}
|
||||
|
||||
const [updatedProject] = await Promise.all([
|
||||
updateMainImage(),
|
||||
replaceSocialCard(),
|
||||
])
|
||||
|
||||
return updatedProject
|
||||
}
|
||||
|
||||
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
|
||||
requireAuth()
|
||||
await requireOwnership({ projectId: id })
|
||||
const project = await db.project.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
const childrenDeletePromises = [
|
||||
db.comment.deleteMany({ where: { projectId: project.id } }),
|
||||
db.projectReaction.deleteMany({ where: { projectId: project.id } }),
|
||||
db.socialCard.deleteMany({ where: { projectId: project.id } }),
|
||||
]
|
||||
await Promise.all(childrenDeletePromises)
|
||||
await db.project.delete({
|
||||
where: { id },
|
||||
})
|
||||
return project
|
||||
}
|
||||
|
||||
export const Project = {
|
||||
forkedFrom: (_obj, { root }) =>
|
||||
root.forkedFromId &&
|
||||
db.project.findUnique({ where: { id: root.forkedFromId } }),
|
||||
childForks: (_obj, { root }) =>
|
||||
db.project.findMany({ where: { forkedFromId: root.id } }),
|
||||
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
db.user.findUnique({ where: { id: root.userId } }),
|
||||
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
db.project.findUnique({ where: { id: root.id } }).socialCard(),
|
||||
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
db.project
|
||||
.findUnique({ where: { id: root.id } })
|
||||
.Comment({ orderBy: { createdAt: 'desc' } }),
|
||||
Reaction: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
db.project
|
||||
.findUnique({ where: { id: root.id } })
|
||||
.Reaction({ where: { userId: _obj.userId } }),
|
||||
}
|
||||
25
app/api/src/services/socialCards/socialCards.ts
Normal file
25
app/api/src/services/socialCards/socialCards.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ResolverArgs, BeforeResolverSpecType } from '@redwoodjs/graphql-server'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
|
||||
// Used when the environment variable REDWOOD_SECURE_SERVICES=1
|
||||
export const beforeResolver = (rules: BeforeResolverSpecType) => {
|
||||
rules.add(requireAuth)
|
||||
}
|
||||
|
||||
export const socialCards = () => {
|
||||
return db.socialCard.findMany()
|
||||
}
|
||||
|
||||
export const socialCard = ({ id }: Prisma.SocialCardWhereUniqueInput) => {
|
||||
return db.socialCard.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const SocialCard = {
|
||||
project: (_obj, { root }: ResolverArgs<ReturnType<typeof socialCard>>) =>
|
||||
db.socialCard.findUnique({ where: { id: root.id } }).project(),
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
import { subjectAccessRequests } from './subjectAccessRequests'
|
||||
*/
|
||||
|
||||
describe('subjectAccessRequests', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
import { users } from './users'
|
||||
*/
|
||||
|
||||
describe('users', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,30 @@
|
||||
import { UserInputError, ForbiddenError } from '@redwoodjs/graphql-server'
|
||||
import { db } from 'src/lib/db'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
import { UserInputError } from '@redwoodjs/api'
|
||||
import { enforceAlphaNumeric, destroyImage } from 'src/services/helpers'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
function userNameVerification(userName: string): string {
|
||||
if (userName.length < 5) {
|
||||
throw new ForbiddenError('userName too short')
|
||||
}
|
||||
if (userName && ['new', 'edit', 'update'].includes(userName)) {
|
||||
//TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
|
||||
throw new UserInputError(
|
||||
`You've tried to used a protected word as you userName, try something other than `
|
||||
)
|
||||
}
|
||||
if (userName) {
|
||||
return enforceAlphaNumeric(userName)
|
||||
}
|
||||
}
|
||||
|
||||
function nameVerification(name: string) {
|
||||
if (typeof name === 'string' && name.length < 3) {
|
||||
throw new ForbiddenError('name too short')
|
||||
}
|
||||
}
|
||||
|
||||
export const users = () => {
|
||||
requireAuth({ role: 'admin' })
|
||||
@@ -25,35 +47,54 @@ export const createUser = ({ input }) => {
|
||||
requireAuth({ role: 'admin' })
|
||||
createUserInsecure({ input })
|
||||
}
|
||||
export const createUserInsecure = ({ input }) => {
|
||||
export const createUserInsecure = ({
|
||||
input,
|
||||
}: {
|
||||
input: Prisma.UserUncheckedCreateInput
|
||||
}) => {
|
||||
if (typeof input.userName === 'string') {
|
||||
input.userName = userNameVerification(input.userName)
|
||||
}
|
||||
nameVerification(input.name)
|
||||
return db.user.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUser = ({ id, input }) => {
|
||||
export const updateUser = ({
|
||||
id,
|
||||
input,
|
||||
}: {
|
||||
id: string
|
||||
input: Prisma.UserUncheckedCreateInput
|
||||
}) => {
|
||||
requireAuth()
|
||||
if (typeof input.userName === 'string') {
|
||||
input.userName = userNameVerification(input.userName)
|
||||
}
|
||||
nameVerification(input.name)
|
||||
return db.user.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUserByUserName = async ({ userName, input }) => {
|
||||
export const updateUserByUserName = async ({
|
||||
userName,
|
||||
input,
|
||||
}: {
|
||||
userName: string
|
||||
input: Prisma.UserUncheckedCreateInput
|
||||
}) => {
|
||||
requireAuth()
|
||||
await requireOwnership({ userName })
|
||||
if (input.userName) {
|
||||
input.userName = enforceAlphaNumeric(input.userName)
|
||||
if (typeof input.userName === 'string') {
|
||||
input.userName = userNameVerification(input.userName)
|
||||
}
|
||||
if (input.userName && ['new', 'edit', 'update'].includes(input.userName)) {
|
||||
//TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
|
||||
throw new UserInputError(
|
||||
`You've tried to used a protected word as you userName, try something other than `
|
||||
)
|
||||
}
|
||||
const originalPart = await db.user.findUnique({ where: { userName } })
|
||||
nameVerification(input.name)
|
||||
const originalProject = await db.user.findUnique({ where: { userName } })
|
||||
const imageToDestroy =
|
||||
originalPart.image !== input.image && originalPart.image
|
||||
originalProject.image !== input.image && originalProject.image
|
||||
const update = await db.user.update({
|
||||
data: input,
|
||||
where: { userName },
|
||||
@@ -73,14 +114,14 @@ export const deleteUser = ({ id }) => {
|
||||
}
|
||||
|
||||
export const User = {
|
||||
Parts: (_obj, { root }) =>
|
||||
db.user.findUnique({ where: { id: root.id } }).Part(),
|
||||
Part: (_obj, { root }) =>
|
||||
_obj.partTitle &&
|
||||
db.part.findUnique({
|
||||
Projects: (_obj, { root }) =>
|
||||
db.user.findUnique({ where: { id: root.id } }).Project(),
|
||||
Project: (_obj, { root }) =>
|
||||
_obj.projectTitle &&
|
||||
db.project.findUnique({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: _obj.partTitle,
|
||||
title: _obj.projectTitle,
|
||||
userId: root.id,
|
||||
},
|
||||
},
|
||||
17
app/api/tsconfig.json
Normal file
17
app/api/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"src/*": ["./src/*"]
|
||||
},
|
||||
"typeRoots": ["../node_modules/@types", "./node_modules/@types"],
|
||||
"types": ["jest"]
|
||||
},
|
||||
"include": ["src", "../.redwood/**/*"]
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
const { getConfig } = require('@redwoodjs/internal')
|
||||
|
||||
const config = getConfig()
|
||||
const { getPaths } = require('@redwoodjs/internal')
|
||||
|
||||
module.exports = {
|
||||
schema: `http://${config.api.host}:${config.api.port}/graphql`,
|
||||
schema: getPaths().generated.schema,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,16 @@ publish = "web/dist"
|
||||
functions = "api/dist/functions"
|
||||
|
||||
[dev]
|
||||
command = "yarn rw dev"
|
||||
# To use [Netlify Dev](https://www.netlify.com/products/dev/),
|
||||
# install netlify-cli from https://docs.netlify.com/cli/get-started/#installation
|
||||
# and then use netlify link https://docs.netlify.com/cli/get-started/#link-and-unlink-sites
|
||||
# to connect your local project to a site already on Netlify
|
||||
# then run netlify dev and our app will be accessible on the port specified below
|
||||
framework = "redwoodjs"
|
||||
# Set targetPort to the [web] side port as defined in redwood.toml
|
||||
targetPort = 8910
|
||||
# Point your browser to this port to access your RedwoodJS app
|
||||
port = 8888
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
@@ -13,3 +22,10 @@ functions = "api/dist/functions"
|
||||
|
||||
[context.deploy-preview.environment]
|
||||
CAD_LAMBDA_BASE_URL = "https://t7wdlz8ztf.execute-api.us-east-1.amazonaws.com/dev2"
|
||||
|
||||
[[plugins]]
|
||||
package = "@sentry/netlify-build-plugin"
|
||||
|
||||
[plugins.inputs]
|
||||
sentryOrg = "kurt"
|
||||
sentryProject = "kurt"
|
||||
|
||||
@@ -6,19 +6,34 @@
|
||||
"web"
|
||||
]
|
||||
},
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"cad": "yarn rw build api && zip-it-and-ship-it api/dist/functions/ api/dist/zipball && docker-compose --file ./api/src/docker/docker-compose.yml up --build",
|
||||
"cad-r": "yarn rw build api && zip-it-and-ship-it api/dist/functions/ api/dist/zipball && docker-compose --file ./api/src/docker/docker-compose.yml restart",
|
||||
"aws-emulate": "nodemon ./api/src/docker/aws-emulator.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redwoodjs/core": "^0.31.0"
|
||||
"@redwoodjs/core": "^0.38.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@redwoodjs/eslint-config"
|
||||
"extends": "@redwoodjs/eslint-config",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": [
|
||||
"error",
|
||||
{
|
||||
"forbid": [
|
||||
">",
|
||||
"}",
|
||||
"\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"node": ">=14.x <=16.x",
|
||||
"yarn": ">=1.15"
|
||||
},
|
||||
"resolutions": {
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1"
|
||||
"prisma": {
|
||||
"seed": "yarn rw exec seed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,27 @@
|
||||
|
||||
[web]
|
||||
port = 8910
|
||||
apiProxyPath = "/.netlify/functions"
|
||||
includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET', 'CAD_LAMBDA_BASE_URL']
|
||||
title = 'CadHub'
|
||||
# apiUrl = "/.netlify/functions"
|
||||
apiUrl = "https://uk5gegwopd.execute-api.us-east-2.amazonaws.com/.netlify/functions"
|
||||
includeEnvironmentVariables = [
|
||||
'GOOGLE_ANALYTICS_ID',
|
||||
'CLOUDINARY_API_KEY',
|
||||
# 'CLOUDINARY_API_SECRET',
|
||||
'CAD_LAMBDA_BASE_URL',
|
||||
'SENTRY_DSN',
|
||||
'SENTRY_AUTH_TOKEN',
|
||||
'SENTRY_ORG',
|
||||
'SENTRY_PROJECT',
|
||||
# 'EMAIL_PASSWORD'
|
||||
]
|
||||
# experimentalFastRefresh = true # this seems to break cascadeStudio
|
||||
[api]
|
||||
port = 8911
|
||||
schemaPath = "./api/db/schema.prisma"
|
||||
[browser]
|
||||
open = true
|
||||
|
||||
[experimental]
|
||||
esbuild = true
|
||||
|
||||
|
||||
235
app/scripts/seed.ts
Normal file
235
app/scripts/seed.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
import { db } from '$api/src/lib/db'
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
const users = [
|
||||
{
|
||||
id: "a2b21ce1-ae57-43a2-b6a3-b6e542fd9e60",
|
||||
userName: "local-user-1",
|
||||
name: "local 1",
|
||||
email: "localUser1@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "682ba807-d10e-4caf-bf28-74054e46c9ec",
|
||||
userName: "local-user-2",
|
||||
name: "local 2",
|
||||
email: "localUser2@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "5cea3906-1e8e-4673-8f0d-89e6a963c096",
|
||||
userName: "local-admin-2",
|
||||
name: "local admin",
|
||||
email: "localAdmin@kurthutten.com"
|
||||
},
|
||||
]
|
||||
|
||||
let existing
|
||||
existing = await db.user.findMany({ where: { id: users[0].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[0],
|
||||
})
|
||||
}
|
||||
existing = await db.user.findMany({ where: { id: users[1].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[1],
|
||||
})
|
||||
}
|
||||
|
||||
const projects = [
|
||||
{
|
||||
title: 'demo-project1',
|
||||
description: '# can be markdown',
|
||||
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
|
||||
code: getOpenScadHingeCode(),
|
||||
cadPackage: 'openscad',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[0].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'demo-project2',
|
||||
description: '## [hey](www.google.com)',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[1].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
existing = await db.project.findMany({where: { title: projects[0].title}})
|
||||
if(!existing.length) {
|
||||
await db.project.create({
|
||||
data: projects[0],
|
||||
})
|
||||
}
|
||||
existing = await db.project.findMany({where: { title: projects[1].title}})
|
||||
if(!existing.length) {
|
||||
const result = await db.project.create({
|
||||
data: projects[1],
|
||||
})
|
||||
|
||||
await db.project.create({
|
||||
data: {
|
||||
...projects[1],
|
||||
title: `${projects[1].title}-fork`,
|
||||
forkedFrom: {
|
||||
connect: {
|
||||
id: result.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const aProject = await db.project.findUnique({where: {
|
||||
title_userId: {
|
||||
title: projects[0].title,
|
||||
userId: users[0].id,
|
||||
}
|
||||
}})
|
||||
await db.comment.create({
|
||||
data: {
|
||||
text: "nice project, I like it",
|
||||
userId: users[0].id,
|
||||
projectId: aProject.id,
|
||||
// user: {connect: { id: users[0].id}},
|
||||
// project: {connect: { id: aProject.id}},
|
||||
}
|
||||
})
|
||||
await db.projectReaction.create({
|
||||
data: {
|
||||
emote: "❤️",
|
||||
userId: users[0].id,
|
||||
projectId: aProject.id,
|
||||
// user: {connect: { id: users[0].id}},
|
||||
// project: {connect: { id: aProject.id}},
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Please define your seed data.')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getOpenScadHingeCode () {
|
||||
return `
|
||||
baseWidth=15; // [0.1:0.1:50]
|
||||
|
||||
hingeLength=30; // [0.1:0.1:50]
|
||||
|
||||
// Hole mant mounting holes per half.
|
||||
mountingHoleCount=3; // [1:20]
|
||||
|
||||
baseThickness=3; // [0.1:0.1:20]
|
||||
|
||||
pivotRadius=5; // [0.1:0.1:20]
|
||||
|
||||
// Pin that the hinge pivots on.
|
||||
pinRadius=2; // [0.1:0.1:20]
|
||||
|
||||
mountingHoleRadius=1.5; // [0.1:0.1:10]
|
||||
|
||||
// How far away the hole is from the egde.
|
||||
mountingHoleEdgeOffset=4; // [0:50]
|
||||
|
||||
// Depending on the accuracy of your printer this may need to be increased in order for print in place to work.
|
||||
clearance=0.2; // [0.05:0.01:1]
|
||||
|
||||
// Radius difference in the ivot taper to stop the hinge from falling apart. Should be increased with large clearance values.
|
||||
pinTaper=0.25; // [0.1:0.1:2]
|
||||
|
||||
// calculated values
|
||||
hingeHalfExtrudeLength=hingeLength/2-clearance/2;
|
||||
mountingHoleMoveIncrement=(hingeLength-2*mountingHoleEdgeOffset)/
|
||||
(mountingHoleCount-1);
|
||||
|
||||
module costomizerEnd() {}
|
||||
$fn=30;
|
||||
tiny=0.005;
|
||||
// modules
|
||||
module hingeBaseProfile() {
|
||||
translate([pivotRadius,0,0]){
|
||||
square([baseWidth,baseThickness]);
|
||||
}
|
||||
}
|
||||
|
||||
module hingeBodyHalf() {
|
||||
difference() {
|
||||
union() {
|
||||
linear_extrude(hingeHalfExtrudeLength){
|
||||
offset(1)offset(-2)offset(1){
|
||||
translate([0,pivotRadius,0]){
|
||||
circle(pivotRadius);
|
||||
}
|
||||
square([pivotRadius,pivotRadius]);
|
||||
hingeBaseProfile();
|
||||
}
|
||||
}
|
||||
linear_extrude(hingeLength){
|
||||
offset(1)offset(-1)hingeBaseProfile();
|
||||
}
|
||||
}
|
||||
plateHoles();
|
||||
}
|
||||
}
|
||||
|
||||
module pin(rotateY, radiusOffset) {
|
||||
translate([0,pivotRadius,hingeHalfExtrudeLength+tiny]){
|
||||
rotate([0,rotateY,0]) {
|
||||
cylinder(
|
||||
h=hingeLength/2+clearance/2,
|
||||
r1=pinRadius+radiusOffset,
|
||||
r2=pinRadius+pinTaper+radiusOffset
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module hingeHalfFemale() {
|
||||
difference() {
|
||||
hingeBodyHalf();
|
||||
pin(rotateY=180, radiusOffset=clearance);
|
||||
}
|
||||
}
|
||||
|
||||
module hingeHalfMale() {
|
||||
translate([0,0,hingeLength]) {
|
||||
rotate([0,180,0]) {
|
||||
hingeBodyHalf();
|
||||
pin(rotateY=0, radiusOffset=0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module plateHoles() {
|
||||
for(i=[0:mountingHoleCount-1]){
|
||||
translate([
|
||||
baseWidth/2+pivotRadius,
|
||||
-baseThickness,
|
||||
i*mountingHoleMoveIncrement+mountingHoleEdgeOffset
|
||||
]){
|
||||
rotate([-90,0,0]){
|
||||
cylinder(r=mountingHoleRadius,h=baseThickness*4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// using high-level modules
|
||||
translate([0,0,-15]) {
|
||||
hingeHalfFemale();
|
||||
hingeHalfMale();
|
||||
}
|
||||
`
|
||||
}
|
||||
188
app/serverless.yml
Normal file
188
app/serverless.yml
Normal file
@@ -0,0 +1,188 @@
|
||||
# See the full yml reference at https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/
|
||||
service: cadhubapi
|
||||
|
||||
# Uncomment org and app if you want to integrate your deployment with the Serverless dashboard. See https://www.serverless.com/framework/docs/dashboard/ for more details.
|
||||
# org: your-org
|
||||
# app: your-app
|
||||
|
||||
plugins:
|
||||
- serverless-dotenv-plugin
|
||||
- serverless-binary-cors
|
||||
- serverless-plugin-git-variables
|
||||
|
||||
custom:
|
||||
dotenv:
|
||||
include:
|
||||
- DATABASE_URL_PROD
|
||||
- CLOUDINARY_API_KEY
|
||||
- CLOUDINARY_API_SECRET
|
||||
- EMAIL_PASSWORD
|
||||
- SENTRY_DSN
|
||||
# - # List the environment variables you want to include from your .env file here.
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
lambdaHashingVersion: 20201221
|
||||
runtime: nodejs14.x
|
||||
region: us-east-2 # This is the AWS region where the service will be deployed.
|
||||
httpApi: # HTTP API is used by default. To learn about the available options in API Gateway, see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html
|
||||
cors: true
|
||||
payload: '1.0'
|
||||
stackTags: # Add CloudFormation stack tags here
|
||||
source: serverless
|
||||
name: Redwood Lambda API with HTTP API Gateway
|
||||
tags: # Add service wide tags here
|
||||
name: Redwood Lambda API with HTTP API Gateway
|
||||
ecr:
|
||||
images:
|
||||
# this image is built locally and push to ECR
|
||||
openscadimage:
|
||||
path: ./
|
||||
file: api/src/docker/openscad/Dockerfile
|
||||
cadqueryimage:
|
||||
path: ./
|
||||
file: api/src/docker/cadquery/Dockerfile
|
||||
apiGateway:
|
||||
metrics: true
|
||||
binaryMediaTypes:
|
||||
# we need to allow binary types to be able to send back images and stls, but it would be better to be more specific
|
||||
# ie image/png etc. as */* treats everything as binary including the json body as the input the lambdas
|
||||
# which mean we need to decode the input bode from base64, but the images break with anything other than */* :(
|
||||
- '*/*'
|
||||
|
||||
package:
|
||||
individually: true
|
||||
|
||||
functions:
|
||||
check-user-name:
|
||||
description: check-user-name function deployed on AWS Lambda
|
||||
package:
|
||||
artifact: api/dist/zipball/check-user-name.zip # This is the default location of the zip file generated during the deploy command.
|
||||
memorySize: 1024 # mb
|
||||
timeout: 25 # seconds (max: 29)
|
||||
tags: # Tags for this specific lambda function
|
||||
endpoint: /.netlify/functions/check-user-name
|
||||
# Uncomment this section to add environment variables either from the Serverless dotenv plugin or using Serverless params
|
||||
environment:
|
||||
SENTRY_DSN: ${env:SENTRY_DSN}
|
||||
DATABASE_URL: ${env:DATABASE_URL_PROD}
|
||||
COMMIT_REF: ${git:sha1}
|
||||
CONTEXT: TODO
|
||||
handler: check-user-name.handler
|
||||
events:
|
||||
- httpApi:
|
||||
path: /.netlify/functions/check-user-name
|
||||
method: GET
|
||||
# cors: true
|
||||
- httpApi:
|
||||
path: /.netlify/functions/check-user-name
|
||||
method: POST
|
||||
# cors: true
|
||||
graphql:
|
||||
description: graphql function deployed on AWS Lambda
|
||||
package:
|
||||
artifact: api/dist/zipball/graphql.zip # This is the default location of the zip file generated during the deploy command.
|
||||
memorySize: 1024 # mb
|
||||
timeout: 25 # seconds (max: 29)
|
||||
tags: # Tags for this specific lambda function
|
||||
endpoint: /.netlify/functions/graphql
|
||||
# Uncomment this section to add environment variables either from the Serverless dotenv plugin or using Serverless params
|
||||
environment:
|
||||
CLOUDINARY_API_KEY: ${env:CLOUDINARY_API_KEY}
|
||||
CLOUDINARY_API_SECRET: ${env:CLOUDINARY_API_SECRET}
|
||||
EMAIL_PASSWORD: ${env:EMAIL_PASSWORD}
|
||||
SENTRY_DSN: ${env:SENTRY_DSN}
|
||||
DATABASE_URL: ${env:DATABASE_URL_PROD}
|
||||
COMMIT_REF: ${git:sha1}
|
||||
CONTEXT: TODO
|
||||
# YOUR_FIRST_ENV_VARIABLE: ${env:YOUR_FIRST_ENV_VARIABLE}
|
||||
handler: graphql.handler
|
||||
events:
|
||||
- httpApi:
|
||||
path: /.netlify/functions/graphql
|
||||
method: GET
|
||||
# cors: true
|
||||
- httpApi:
|
||||
path: /.netlify/functions/graphql
|
||||
method: POST
|
||||
# cors: true
|
||||
# identity-signup: # this is netlify specific and is related to go true auth, so we'll continue having that deployed on netlify
|
||||
# description: identity-signup function deployed on AWS Lambda
|
||||
# package:
|
||||
# artifact: api/dist/zipball/identity-signup.zip # This is the default location of the zip file generated during the deploy command.
|
||||
# memorySize: 1024 # mb
|
||||
# timeout: 25 # seconds (max: 29)
|
||||
# tags: # Tags for this specific lambda function
|
||||
# endpoint: /.netlify/functions/identity-signup
|
||||
# # Uncomment this section to add environment variables either from the Serverless dotenv plugin or using Serverless params
|
||||
# # environment:
|
||||
# # YOUR_FIRST_ENV_VARIABLE: ${env:YOUR_FIRST_ENV_VARIABLE}
|
||||
# handler: identity-signup.handler
|
||||
# events:
|
||||
# - httpApi:
|
||||
# path: /.netlify/functions/identity-signup
|
||||
# method: GET
|
||||
# - httpApi:
|
||||
# path: /.netlify/functions/identity-signup
|
||||
# method: POST
|
||||
openscadpreview:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.preview
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: openscad/preview
|
||||
method: post
|
||||
cors: true
|
||||
timeout: 25
|
||||
openscadstl:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.stl
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: openscad/stl
|
||||
method: post
|
||||
cors: true
|
||||
timeout: 30
|
||||
cadquerystl:
|
||||
image:
|
||||
name: cadqueryimage
|
||||
command:
|
||||
- cadquery.stl
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: cadquery/stl
|
||||
method: post
|
||||
cors: true
|
||||
timeout: 30
|
||||
|
||||
# this allows browsers to see error responses.
|
||||
resources:
|
||||
Resources:
|
||||
GatewayResponseDefault4XX:
|
||||
Type: 'AWS::ApiGateway::GatewayResponse'
|
||||
Properties:
|
||||
ResponseParameters:
|
||||
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
|
||||
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
|
||||
ResponseType: DEFAULT_4XX
|
||||
RestApiId:
|
||||
Ref: 'ApiGatewayRestApi'
|
||||
GatewayResponseDefault5XX:
|
||||
Type: 'AWS::ApiGateway::GatewayResponse'
|
||||
Properties:
|
||||
ResponseParameters:
|
||||
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
|
||||
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
|
||||
ResponseType: DEFAULT_5XX
|
||||
RestApiId:
|
||||
Ref: 'ApiGatewayRestApi'
|
||||
@@ -2,7 +2,8 @@ const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('tailwindcss')(path.resolve(__dirname, '../tailwind.config.js')),
|
||||
require('postcss-import'),
|
||||
require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.js')),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,15 +1,75 @@
|
||||
module.exports = {
|
||||
purge: ['./src/**/*.html', './src/**/*.js', './src/**/*.ts', './src/**/*.tsx'],
|
||||
purge: ['src/**/*.{js,jsx,ts,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: () => ({
|
||||
texture: `url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236a49c3' fill-opacity='0.47' fill-rule='evenodd'/%3E%3C/svg%3E");`,
|
||||
}),
|
||||
animation: {
|
||||
'bounce-sm-slow': 'bounce-sm 5s linear infinite',
|
||||
'twist-sm-slow': 'twist-sm 10s infinite',
|
||||
},
|
||||
backgroundImage: () => ({
|
||||
texture: `url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236a49c3' fill-opacity='0.47' fill-rule='evenodd'/%3E%3C/svg%3E");`,
|
||||
}),
|
||||
borderRadius: {
|
||||
half: '50%',
|
||||
},
|
||||
boxShadow: {
|
||||
ch: '0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 4px 0 rgba(13, 13, 19, 0.15)',
|
||||
},
|
||||
colors: {
|
||||
'ch-gray': {
|
||||
900: '#0D0D13',
|
||||
800: '#1A1A1D',
|
||||
750: '#222222',
|
||||
760: '#232532',
|
||||
710: '#2B303C', // TODO: Use HSL so I stop adding grays to fix the warm/cool problem
|
||||
700: '#2A3038',
|
||||
600: '#3B3E4B',
|
||||
550: '#63636A',
|
||||
500: '#9F9FB4',
|
||||
400: '#A4A4B0',
|
||||
300: '#CFCFD8',
|
||||
},
|
||||
'ch-purple': {
|
||||
400: '#3B0480',
|
||||
450: '#671BC6',
|
||||
500: '#8732F2',
|
||||
600: '#A663FA',
|
||||
200: '#C99DFF',
|
||||
},
|
||||
'ch-purple-gray': {
|
||||
200: '#DBDBEC',
|
||||
},
|
||||
'ch-blue': {
|
||||
700: '#08466F',
|
||||
650: '#0958BA',
|
||||
640: '#0A57B5',
|
||||
630: '#3285EB',
|
||||
500: '#5098F1',
|
||||
400: '#79B2F8',
|
||||
300: '#9BC8FF',
|
||||
},
|
||||
'ch-pink': {
|
||||
800: '#93064F',
|
||||
500: '#DF5CA0',
|
||||
300: '#F98CC5',
|
||||
},
|
||||
},
|
||||
cursor: {
|
||||
grab: 'grab'
|
||||
},
|
||||
fontFamily: {
|
||||
'ropa-sans': ['"Ropa Sans"', 'Arial', 'sans-serif'],
|
||||
roboto: ['Roboto', 'Arial', 'sans-serif'],
|
||||
'fira-code': ['"Fira Code"', 'monospace'],
|
||||
'fira-sans': ['"Fira Sans"', 'sans-serif'],
|
||||
},
|
||||
gridAutoColumns: {
|
||||
'preview-layout': 'minmax(30rem, 1fr) minmax(auto, 2fr)',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
'profile-layout': 'minmax(32rem, 1fr) 2fr',
|
||||
},
|
||||
keyframes: {
|
||||
'bounce-sm': {
|
||||
'0%, 100%': {
|
||||
@@ -38,18 +98,16 @@ module.exports = {
|
||||
minHeight: {
|
||||
md: '28rem',
|
||||
},
|
||||
fontFamily: {
|
||||
'ropa-sans': ['Ropa Sans', 'Arial', 'sans-serif'],
|
||||
roboto: ['Roboto', 'Arial', 'sans-serif'],
|
||||
outline: {
|
||||
gray: ['2px solid #3B3E4B', '8px'],
|
||||
},
|
||||
skew: {
|
||||
'-20': '-20deg',
|
||||
},
|
||||
borderRadius: {
|
||||
half: '50%',
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
],
|
||||
}
|
||||
@@ -1,92 +1,13 @@
|
||||
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
|
||||
|
||||
module.exports = (config, { env }) => {
|
||||
config.plugins.forEach((plugin) => {
|
||||
if (plugin.constructor.name === 'HtmlWebpackPlugin') {
|
||||
plugin.options.favicon = './src/favicon.svg'
|
||||
} else if (plugin.constructor.name === 'CopyPlugin') {
|
||||
plugin.patterns.push({
|
||||
from: './src/cascade/js/StandardLibraryIntellisense.ts',
|
||||
to: 'js/StandardLibraryIntellisense.ts',
|
||||
})
|
||||
plugin.patterns.push({
|
||||
from: './src/cascade/static_node_modules/opencascade.js/dist/oc.d.ts',
|
||||
to: 'opencascade.d.ts',
|
||||
})
|
||||
plugin.patterns.push({
|
||||
from: '../node_modules/three/src/Three.d.ts',
|
||||
to: 'Three.d.ts',
|
||||
})
|
||||
plugin.patterns.push({
|
||||
from: './src/cascade/fonts',
|
||||
to: 'fonts',
|
||||
})
|
||||
plugin.patterns.push({
|
||||
from: './src/cascade/textures',
|
||||
to: 'textures',
|
||||
})
|
||||
plugin.userOptions.favicon = './src/favicon.svg'
|
||||
}
|
||||
})
|
||||
config.plugins.push(
|
||||
new MonacoWebpackPlugin({
|
||||
languages: ['typescript'],
|
||||
features: [
|
||||
'accessibilityHelp',
|
||||
'anchorSelect',
|
||||
'bracketMatching',
|
||||
'caretOperations',
|
||||
'clipboard',
|
||||
'codeAction',
|
||||
'codelens',
|
||||
'comment',
|
||||
'contextmenu',
|
||||
'coreCommands',
|
||||
'cursorUndo',
|
||||
'documentSymbols',
|
||||
'find',
|
||||
'folding',
|
||||
'fontZoom',
|
||||
'format',
|
||||
'gotoError',
|
||||
'gotoLine',
|
||||
'gotoSymbol',
|
||||
'hover',
|
||||
'inPlaceReplace',
|
||||
'indentation',
|
||||
'inlineHints',
|
||||
'inspectTokens',
|
||||
'linesOperations',
|
||||
'linkedEditing',
|
||||
'links',
|
||||
'multicursor',
|
||||
'parameterHints',
|
||||
'quickCommand',
|
||||
'quickHelp',
|
||||
'quickOutline',
|
||||
'referenceSearch',
|
||||
'rename',
|
||||
'smartSelect',
|
||||
'snippets',
|
||||
'suggest',
|
||||
'toggleHighContrast',
|
||||
'toggleTabFocusMode',
|
||||
'transpose',
|
||||
'unusualLineTerminators',
|
||||
'viewportSemanticTokens',
|
||||
'wordHighlighter',
|
||||
'wordOperations',
|
||||
'wordPartOperations',
|
||||
],
|
||||
})
|
||||
)
|
||||
config.module.rules[0].oneOf.push({
|
||||
test: /opencascade\.wasm\.wasm$/,
|
||||
type: 'javascript/auto',
|
||||
loader: 'file-loader',
|
||||
})
|
||||
config.node = {
|
||||
fs: 'empty',
|
||||
}
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.(md|jscad\.js|py|scad)$/i,
|
||||
use: 'raw-loader',
|
||||
});
|
||||
return config
|
||||
}
|
||||
|
||||
10
app/web/config/worker-loader.d.ts
vendored
Normal file
10
app/web/config/worker-loader.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare module "worker-loader!*" {
|
||||
// You need to change `Worker`, if you specified a different value for the `workerType` option
|
||||
class WebpackWorker extends Worker {
|
||||
constructor();
|
||||
}
|
||||
|
||||
// Uncomment this if you set the `esModule` option to `false`
|
||||
// export = WebpackWorker;
|
||||
export default WebpackWorker;
|
||||
}
|
||||
@@ -1,6 +1 @@
|
||||
const { getConfig } = require('@redwoodjs/core')
|
||||
|
||||
const config = getConfig({ type: 'jest', target: 'browser' })
|
||||
config.displayName.name = 'web'
|
||||
|
||||
module.exports = config
|
||||
module.exports = require('@redwoodjs/testing/config/jest/api')
|
||||
|
||||
@@ -13,43 +13,53 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@monaco-editor/react": "^4.0.11",
|
||||
"@redwoodjs/auth": "^0.31.0",
|
||||
"@redwoodjs/forms": "^0.31.0",
|
||||
"@redwoodjs/router": "^0.31.0",
|
||||
"@redwoodjs/web": "^0.31.0",
|
||||
"@react-three/drei": "^7.3.1",
|
||||
"@react-three/fiber": "^7.0.5",
|
||||
"@react-three/postprocessing": "^2.0.5",
|
||||
"@redwoodjs/auth": "^0.38.1",
|
||||
"@redwoodjs/forms": "^0.38.1",
|
||||
"@redwoodjs/router": "^0.38.1",
|
||||
"@redwoodjs/web": "^0.38.1",
|
||||
"@sentry/browser": "^6.5.1",
|
||||
"@tailwindcss/aspect-ratio": "0.2.1",
|
||||
"axios": "^0.21.1",
|
||||
"browser-fs-access": "^0.17.2",
|
||||
"cloudinary-react": "^1.6.7",
|
||||
"controlkit": "^0.1.9",
|
||||
"get-active-classes": "^0.0.11",
|
||||
"golden-layout": "^1.5.9",
|
||||
"gotrue-js": "^0.9.27",
|
||||
"jquery": "^3.5.1",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"monaco-editor-webpack-plugin": "^1.9.1",
|
||||
"hotkeys-js": "^3.8.7",
|
||||
"html-to-image": "^1.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"netlify-identity-widget": "^1.9.1",
|
||||
"opencascade.js": "^0.1.15",
|
||||
"pako": "^2.0.3",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^11.2.1",
|
||||
"react-ga": "^3.3.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hotkeys-hook": "^3.4.0",
|
||||
"react-image-crop": "^8.6.6",
|
||||
"react-mosaic-component": "^4.1.1",
|
||||
"react-three-fiber": "^5.3.19",
|
||||
"react-intersection-observer": "^8.32.1",
|
||||
"react-mosaic-component": "^5.0.0",
|
||||
"react-tabs": "^3.2.2",
|
||||
"rich-markdown-editor": "^11.0.2",
|
||||
"styled-components": "^5.2.0",
|
||||
"three": "^0.118.3"
|
||||
"three": "^0.130.1",
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"postcss": "^8.2.13",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"@types/lodash": "^4.14.170",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"opentype.js": "^1.3.3",
|
||||
"postcss-loader": "4.0.2",
|
||||
"tailwindcss": "^2.1.2",
|
||||
"worker-loader": "^3.0.7"
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-loader": "^6.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^2.2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/web/public/default-social-image.jpg
Normal file
BIN
app/web/public/default-social-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
app/web/public/hinge.stl
Normal file
BIN
app/web/public/hinge.stl
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user