Compare commits
564 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f804bb9a | ||
|
|
0b78772d0b | ||
|
|
9e3c2d1cca | ||
|
|
1b097163a4 | ||
|
|
c61d17baa7 | ||
|
|
be8867e12f | ||
|
|
00f98151c1 | ||
|
|
5d78d8b5dd | ||
|
|
0bd12bbf34 | ||
|
|
88cb7b2b65 | ||
|
|
69329118f7 | ||
|
|
2781276c12 | ||
|
|
a24a960184 | ||
|
|
9fd70d3110 | ||
|
|
c1d714e02a | ||
|
|
c08a1a2c74 | ||
|
|
23a1ff8d19 | ||
|
|
b80167001f | ||
|
|
8c2a53aa41 | ||
|
|
dcca780f2d | ||
|
|
9150817c05 | ||
|
|
d57606dd53 | ||
|
|
52f2c94fb7 | ||
|
|
1952625098 | ||
|
|
10dbfda8a4 | ||
|
|
234335cf61 | ||
|
|
52d40b43f0 | ||
|
|
50522bbe63 | ||
|
|
ce927ea1a4 | ||
|
|
aebf409cea | ||
|
|
5f0ea41c04 | ||
|
|
d3c7e4a0a1 | ||
|
|
a3893bdb2b | ||
|
|
4ba4303b1e | ||
| e779876f54 | |||
| bd9edaa60e | |||
|
|
f2ad34c8ab | ||
|
|
131d7bf688 | ||
|
|
d44e75a83a | ||
| e487d6d91e | |||
| 7a63a66da5 | |||
| 17f64cd849 | |||
| ec17aa741f | |||
|
|
38110d2840 | ||
|
|
fd60f2d171 | ||
|
|
86b2c1be50 | ||
|
|
627b8df658 | ||
|
|
3f0d0d523b | ||
|
|
29a9ffce74 | ||
|
|
a69e5ac009 | ||
|
|
caa3322676 | ||
|
|
358841abd5 | ||
|
|
64e7dbc71e | ||
|
|
5a0809c1d0 | ||
|
|
4b5e3d2b04 | ||
|
|
5f24c6e7bd | ||
|
|
8bdf8ce334 | ||
|
|
9012a6a9d8 | ||
|
|
c5fd4aa7d5 | ||
|
|
65cd04a494 | ||
|
|
c79ae7c6e8 | ||
|
|
ddc97f0923 | ||
|
|
a9b902a427 | ||
|
|
96d8e649c8 | ||
|
|
22c93b7571 | ||
|
|
0644d4b580 | ||
|
|
ee6a76cdd9 | ||
|
|
934010a0c1 | ||
|
|
29b2bedae0 | ||
|
|
5ba815590a | ||
|
|
dd09827d08 | ||
| b5b94adc83 | |||
| 3c04e8bb39 | |||
| 17a4328af5 | |||
| e81f2c1f75 | |||
| f77874bec4 | |||
| cfc72b8bc1 | |||
| 359b20fc6d | |||
| a3659618ea | |||
| fa60fc65a9 | |||
| b1727b7838 | |||
| a3f4703dae | |||
| 038918c212 | |||
| 42a947dfb0 | |||
| 5525110d39 | |||
| 7160b77607 | |||
| b5183f84b4 | |||
|
|
7a2b877714 | ||
|
|
9416393618 | ||
|
|
eb245118dc | ||
|
|
13050e52f9 | ||
|
|
5ef3885f72 | ||
|
|
a103666caf | ||
|
|
29da5c2788 | ||
|
|
40f16ab9ca | ||
|
|
a33d56bd61 | ||
|
|
c7c9250594 | ||
|
|
1b1659fe92 | ||
|
|
3c9d71a757 | ||
|
|
342099157e | ||
|
|
bb7a17fc22 | ||
|
|
1880b89b0c | ||
|
|
e769ff1f13 | ||
|
|
0ea8cb86bb | ||
|
|
90f9574a6f | ||
|
|
f2f7ec3f8d | ||
|
|
9e7873cdd7 | ||
|
|
f46c2cfb4a | ||
|
|
9f14061efd | ||
|
|
88b111529b | ||
|
|
851ee7420f | ||
|
|
ef57eb752d | ||
|
|
fcb29ae484 | ||
|
|
5c4847ae2c | ||
|
|
60a73781bd | ||
|
|
4e3b378d6a | ||
|
|
2bf1e783a9 | ||
|
|
375d36f6c5 | ||
|
|
495380ec43 | ||
|
|
af0531bb0c | ||
|
|
c5124fa6ad | ||
|
|
962cf58e77 | ||
|
|
60988dd599 | ||
|
|
004a541302 | ||
|
|
f4cd9e18ea | ||
|
|
2dc301addf | ||
|
|
e85a959c26 | ||
|
|
5fc937d81b | ||
|
|
b3853646cb | ||
|
|
339e808d27 | ||
|
|
22d1a97abd | ||
|
|
ce4baa61dc | ||
|
|
e90c7f05a8 | ||
|
|
fb0e43af88 | ||
|
|
4577997b1c | ||
|
|
9bb256f2ee | ||
|
|
d3994ff26e | ||
|
|
00d097f643 | ||
|
|
99da77f23e | ||
|
|
7a6dc8b0c9 | ||
|
|
b4f04f9b71 | ||
|
|
9df0c98100 | ||
|
|
a47f8744f8 | ||
|
|
dac9849ef5 | ||
|
|
11ed8f90fd | ||
|
|
5d103c6687 | ||
| 80329e240e | |||
| 70b109e78b | |||
| a6a96d6a1e | |||
| cc4b69ca50 | |||
|
|
e733c6acc8 | ||
|
|
afa6f421d3 | ||
|
|
7d7f886661 | ||
|
|
be926dcaed | ||
|
|
fd22b8afe5 | ||
|
|
3353a17611 | ||
|
|
1c1596b44a | ||
|
|
9b05dc3ae3 | ||
|
|
d717269563 | ||
|
|
cba8815cfc | ||
|
|
647f7b2676 | ||
|
|
ef4f2355bf | ||
|
|
24a226b283 | ||
|
|
81717ec5b1 | ||
|
|
f9cb289eff | ||
|
|
022490ae10 | ||
|
|
ca4818c070 | ||
|
|
fe8e9cb262 | ||
|
|
9683d83298 | ||
|
|
69d9a4c499 | ||
| 7678776872 | |||
|
|
f590b573fb | ||
|
|
2c9ec4a7d3 | ||
|
|
393782b4b8 | ||
|
|
c33e1bbaa3 | ||
|
|
63a9271617 | ||
|
|
6469d4763a | ||
|
|
922e36093e | ||
|
|
81976206f9 | ||
|
|
4ac6369deb | ||
|
|
dc0c7fa4e7 | ||
|
|
61ebf58631 | ||
|
|
1d61b1e652 | ||
|
|
d0f9c4a032 | ||
|
|
27119056a4 | ||
|
|
044dd59d8f | ||
|
|
e5ab9b9310 | ||
|
|
f11cddf55a | ||
|
|
f076bf9794 | ||
|
|
fe510e148a | ||
|
|
0a84c9daac | ||
|
|
f496ae5bc1 | ||
|
|
1379cbd3f6 | ||
|
|
ab221bd393 | ||
|
|
e5fb1dfb7e | ||
|
|
c113b70fee | ||
|
|
d2a8f9a1ef | ||
|
|
ee56a53b40 | ||
| ece93f79b2 | |||
| 14e241db37 | |||
| 3becdcff46 | |||
| c0bc611268 | |||
| eff5eae706 | |||
| 59a48ad060 | |||
|
|
d3f7eded41 | ||
|
|
bbf3a317ec | ||
|
|
c6365113c4 | ||
|
|
454835338f | ||
|
|
3f0c2472cb | ||
| a36afa3a47 | |||
| 9bce8d74c9 | |||
|
|
e5acd56b0f | ||
|
|
685e79d76b | ||
|
|
183dee193c | ||
|
|
7167b49edc | ||
|
|
8b465731f0 | ||
|
|
0e26dbfc65 | ||
|
|
347c075ab1 | ||
|
|
01829c7b8b | ||
|
|
e148f9edb8 | ||
|
|
8a00b99f9a | ||
|
|
4d16723e38 | ||
|
|
683984efe9 | ||
|
|
6018028afd | ||
|
|
eac21844c4 | ||
|
|
0cb8dd2693 | ||
|
|
a4a10eb7f2 | ||
|
|
ff4926fa80 | ||
|
|
dd581a8418 | ||
|
|
5f0d7dda59 | ||
|
|
dfdbbdc51c | ||
|
|
72f17c018e | ||
|
|
397dfbcf5f | ||
|
|
df682327d6 | ||
|
|
46d5614e4c | ||
|
|
7e1f03af57 | ||
|
|
b54032fe63 | ||
|
|
b417076ee6 | ||
|
|
95da2cc500 | ||
|
|
84ea0b3743 | ||
|
|
00433ee7ba | ||
|
|
7f282e2ec5 | ||
|
|
3b89387b12 | ||
|
|
3b24cefd3f | ||
|
|
4de420e4dc | ||
|
|
36041369db | ||
|
|
b33ff55167 | ||
|
|
e8e6012bf2 | ||
|
|
92169bf485 | ||
|
|
9f57e8ac67 | ||
|
|
262353376c | ||
|
|
fd50b2268b | ||
|
|
6839cda5b8 | ||
|
|
d2aca488ad | ||
|
|
1fe7491bcd | ||
|
|
6a8fe074e0 | ||
|
|
624b640e01 | ||
|
|
ce4e09f1f6 | ||
|
|
c085e9aa22 | ||
|
|
dc491983f5 | ||
|
|
a0587fbad6 | ||
|
|
702caed232 | ||
|
|
cb65e08465 | ||
|
|
c1e862e6bd | ||
|
|
533dc0e7ad | ||
|
|
ecac53516e | ||
|
|
9133a369d5 | ||
|
|
4c580f1693 | ||
|
|
732f8e2577 | ||
|
|
4b3ec157c2 | ||
|
|
58de04924e | ||
|
|
66048ca793 | ||
|
|
b0b5579cb3 | ||
|
|
005cc7410f | ||
|
|
a4c2c4932d | ||
|
|
617d31cb22 | ||
|
|
384fb10f54 | ||
|
|
72c615ffed | ||
|
|
ce2da1d859 | ||
|
|
9d6beb74c0 | ||
|
|
1f25521900 | ||
|
|
f5c0d6967b | ||
|
|
c5e5519426 | ||
|
|
4c98759ded | ||
|
|
c910b0e617 | ||
|
|
94218558a7 | ||
|
|
f1662ca18b | ||
|
|
4a5658c4ca | ||
|
|
61ed8855ea | ||
|
|
94838ef1fc | ||
|
|
3ce69228a8 | ||
|
|
c522258d04 | ||
|
|
f91ab4c430 | ||
|
|
57cba61d1b | ||
|
|
3fbcb23089 | ||
|
|
9b0c633a87 | ||
|
|
c91bbfd2f1 | ||
|
|
9882fd240e | ||
|
|
ea6073eb71 | ||
|
|
22722082eb | ||
|
|
36316b0333 | ||
|
|
a814eec2cf | ||
|
|
4c96f78a46 | ||
|
|
cc65a3bd09 | ||
|
|
5f9e9f5327 | ||
|
|
b1d54d8665 | ||
|
|
d01aabe788 | ||
|
|
19d64c1bc5 | ||
|
|
ee98e6e352 | ||
|
|
bf52e7385b | ||
|
|
2d6fd3a3dc | ||
|
|
4bb5a11fff | ||
|
|
d4a758d262 | ||
|
|
9397de8cb9 | ||
|
|
d2e1ba51c6 | ||
|
|
ebed646c07 | ||
|
|
7067fb9708 | ||
|
|
e499bb2f9f | ||
|
|
b87ec1dd44 | ||
|
|
77f0c2f06f | ||
|
|
4c1891fb44 | ||
|
|
b3dade1a38 | ||
|
|
be2617e1ee | ||
|
|
35e1268f36 | ||
|
|
a8a3ed0e7b | ||
|
|
0eef957a90 | ||
|
|
6a8ca7d0fa | ||
|
|
ddd29f5530 | ||
|
|
b6e8b20168 | ||
| bfb6cf5958 | |||
| a92ca75760 | |||
| 8d8323e382 | |||
| 76d7e69d19 | |||
| be58e932a9 | |||
| 38bbe56e9b | |||
| a65ce6595a | |||
|
|
96c43bcbad | ||
|
|
ab1ad17d21 | ||
|
|
90f7890e5f | ||
|
|
5c85296810 | ||
|
|
06bfc181c7 | ||
|
|
0473665bb4 | ||
|
|
f610de3045 | ||
|
|
911e174aef | ||
|
|
6d7f46c425 | ||
|
|
b72e7a54e5 | ||
|
|
d99d134382 | ||
|
|
576675411a | ||
|
|
d214558bc4 | ||
|
|
4299a93afe | ||
|
|
920126a392 | ||
|
|
16e6a5e21b | ||
|
|
9539018b64 | ||
|
|
0081eb2acd | ||
|
|
bcb0825f5a | ||
|
|
18a3fa518c | ||
|
|
0407f5c29e | ||
|
|
6dafe2a8e9 | ||
|
|
4a8f0aa1af | ||
|
|
745b20358d | ||
|
|
0f544b31f3 | ||
|
|
76d70f3edd | ||
|
|
1c17ac8b13 | ||
|
|
232579e75b | ||
|
|
01221eda00 | ||
|
|
b73c2fef58 | ||
|
|
3c9c1b5ff7 | ||
|
|
e50b1c1344 | ||
|
|
6dfc531891 | ||
|
|
b4f268dee0 | ||
|
|
1228eb603e | ||
|
|
3ca17338e8 | ||
|
|
614ce105bd | ||
|
|
e366fa4b32 | ||
|
|
cd87451208 | ||
|
|
845c473ed5 | ||
|
|
f4d75eef73 | ||
|
|
5395bbb03a | ||
|
|
a0ca945c72 | ||
|
|
291d7698d4 | ||
|
|
e8956c50ee | ||
|
|
2d90c6eec1 | ||
|
|
0b0fd0585d | ||
|
|
6cf72dfcca | ||
|
|
a81c0b83bb | ||
|
|
b2fb497ecf | ||
|
|
450fe1e7bd | ||
|
|
fbf4dfcfa5 | ||
|
|
d251929ede | ||
| 445816dfad | |||
|
|
4a4f9e2a55 | ||
| 3860c9f72a | |||
| b02b23a978 | |||
| 5b0c1f8992 | |||
|
|
8155549031 | ||
|
|
1ca4633360 | ||
|
|
bb304fa8cd | ||
|
|
9a1f1f78cb | ||
|
|
96bb830600 | ||
|
|
1333b74001 | ||
|
|
ece87dbdb9 | ||
|
|
e82a6b1dd6 | ||
|
|
cd2e119dc6 | ||
|
|
c9928f1cce | ||
|
|
7aac3922d6 | ||
|
|
82403c811e | ||
|
|
230c60bcd0 | ||
|
|
177e903b07 | ||
|
|
a11c236753 | ||
|
|
29ef585410 | ||
|
|
f8be2c2462 | ||
|
|
7d27af1e2d | ||
|
|
258fe91ae7 | ||
|
|
711b5d583b | ||
|
|
4416808056 | ||
|
|
979c27c087 | ||
|
|
b3117886cf | ||
|
|
1c248fa479 | ||
|
|
ec62f4b085 | ||
|
|
04bad30aaa | ||
|
|
e5a52d0f94 | ||
|
|
68c6c6fa11 | ||
|
|
94a64d16e6 | ||
| 7aa7f50ecb | |||
| ee8e0e26db | |||
| 31b965e8f6 | |||
| 94658d4379 | |||
|
|
49a735631a | ||
|
|
1905daec60 | ||
|
|
7a1f4fb787 | ||
|
|
f3cdba34fb | ||
|
|
5b7cb6746d | ||
|
|
6e3e73982f | ||
|
|
8e5c65e6f2 | ||
|
|
94875d4c7f | ||
|
|
e817021ede | ||
|
|
dcca1b1f1c | ||
|
|
c0c2918e72 | ||
|
|
973f9bf5b3 | ||
|
|
162fc9148f | ||
|
|
57d646f6eb | ||
|
|
6768b0b2a6 | ||
|
|
fa14d1f979 | ||
|
|
c4ca2e509e | ||
|
|
1abfbf391f | ||
|
|
073ff033f3 | ||
|
|
23e5941700 | ||
|
|
027d450579 | ||
|
|
ad9bbbc2b9 | ||
|
|
58af78b1d3 | ||
|
|
09d2da8eec | ||
|
|
8abaaf6624 | ||
|
|
3c3697be61 | ||
|
|
073c00a35e | ||
|
|
58d761c359 | ||
|
|
aaaf73f632 | ||
|
|
f83043a9c9 | ||
|
|
cea6d8d0bc | ||
|
|
607c35b621 | ||
|
|
13d0be4586 | ||
|
|
3e1e41f117 | ||
|
|
8f9d7e4a85 | ||
|
|
1e504c8982 | ||
|
|
e56436db3a | ||
|
|
bc227fb0ea | ||
|
|
49bc4f9f45 | ||
|
|
73076c4b28 | ||
|
|
8732972b3f | ||
|
|
cd9d64e501 | ||
|
|
62bf7ec035 | ||
|
|
659f5d5d84 | ||
|
|
bbc53f04de | ||
|
|
431427d7ad | ||
|
|
611ab57c5d | ||
| bc13c10f1a | |||
| 91c9e2b295 | |||
| 585be2aa19 | |||
| 654022b48a | |||
| afab03baf8 | |||
| a52c10fc2c | |||
| f2ed598865 | |||
| 02fc8175f4 | |||
|
|
628e50a48d | ||
|
|
70ab56ce3a | ||
|
|
1fefe7912d | ||
|
|
c21f5f0659 | ||
|
|
46ef0a7f1b | ||
|
|
b43c64962a | ||
|
|
64640eda55 | ||
|
|
a6d9cb3b40 | ||
|
|
b61541f7b8 | ||
|
|
3ff523560b | ||
|
|
b61968706d | ||
|
|
2f27278d3a | ||
|
|
e1ab9fe118 | ||
|
|
b1d0415ba0 | ||
|
|
8ab85ab689 | ||
|
|
16cd794e3b | ||
|
|
f85c30a53b | ||
|
|
6da96ed886 | ||
|
|
852fbd5c87 | ||
|
|
5cec62d1b1 | ||
|
|
7e866f9826 | ||
|
|
2f50f694f3 | ||
|
|
e0f2674811 | ||
|
|
b84ee11f45 | ||
|
|
a2494ce498 | ||
|
|
b76d496034 | ||
|
|
a81d3ee34d | ||
|
|
85473ae492 | ||
|
|
9655e986ff | ||
|
|
101ea8498b | ||
|
|
7d33f85cbc | ||
|
|
66d792715e | ||
|
|
40581f4a45 | ||
|
|
2ca3fcb81a | ||
|
|
30fcacbec6 | ||
|
|
7c3289ccec | ||
|
|
7438986bcd | ||
|
|
3ac017a5f0 | ||
|
|
8e5cc1bc44 | ||
|
|
125a7faf02 | ||
|
|
c9d3ef88e7 | ||
|
|
0ba3bec5aa | ||
|
|
539c35c903 | ||
|
|
e1463d41b9 | ||
|
|
01394056a6 | ||
|
|
1396fcb39c | ||
|
|
c81f8df61c | ||
|
|
1255343b97 | ||
|
|
f7562c18bd | ||
|
|
a3676fabf8 | ||
|
|
dc398d6e06 | ||
|
|
d5da112a01 | ||
|
|
96048bd671 | ||
|
|
dcdc6b196d | ||
|
|
9f542fc9dd | ||
| 930191569f | |||
| 74cd9c0df2 | |||
| d2642b4fb8 | |||
| ebcc48cc57 | |||
| 95b08935cc | |||
| 04487c9b24 | |||
|
|
20eb62d19b | ||
|
|
567d3250e2 | ||
| 4207d5ee50 | |||
|
|
2ec95dd3c3 | ||
|
|
c0d9ee7ca6 | ||
|
|
bf09a25eb5 | ||
|
|
373128ba53 | ||
|
|
3a09d10d3b | ||
|
|
87de52cae0 | ||
|
|
931fe13eee | ||
|
|
28716eeab2 | ||
|
|
27f7945289 | ||
|
|
5a190f3b96 | ||
|
|
606af3901c | ||
|
|
b2247e79ae | ||
|
|
3d76834f45 | ||
| ccc86895e2 | |||
| 279d16d59a | |||
| 04d288b844 | |||
|
|
a6ae770194 | ||
|
|
e378465126 |
@@ -8,6 +8,6 @@ insert_final_newline = true
|
|||||||
indent_style = tab
|
indent_style = tab
|
||||||
indent_size = tab
|
indent_size = tab
|
||||||
|
|
||||||
[{*.yaml,*.yml}]
|
[{*.yaml,*.yml,*.nix}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|||||||
17
.env.example
17
.env.example
@@ -7,4 +7,19 @@ JWT_SECRET=wow
|
|||||||
POSTGRES_DB=chromacase
|
POSTGRES_DB=chromacase
|
||||||
API_URL=http://localhost:80/api
|
API_URL=http://localhost:80/api
|
||||||
SCORO_URL=ws://localhost:6543
|
SCORO_URL=ws://localhost:6543
|
||||||
|
MINIO_ROOT_PASSWORD=12345678
|
||||||
|
EXPO_PUBLIC_API_URL=http://localhost:80/api
|
||||||
|
EXPO_PUBLIC_SCORO_URL=ws://localhost:6543
|
||||||
|
GOOGLE_CLIENT_ID=toto
|
||||||
|
GOOGLE_SECRET=tata
|
||||||
|
GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
|
||||||
|
SMTP_TRANSPORT=smtps://toto:tata@relay
|
||||||
|
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
||||||
|
IGNORE_MAILS=true
|
||||||
|
API_KEYS=SCOROTEST,ROBOTO,SCORO,POPULATE
|
||||||
|
API_KEY_SCORO_TEST=SCOROTEST
|
||||||
|
API_KEY_ROBOT=ROBOTO
|
||||||
|
API_KEY_SCORO=SCORO
|
||||||
|
API_KEY_POPULATE=POPULATE
|
||||||
|
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
|
||||||
|
# vi: ft=sh
|
||||||
|
|||||||
5
.envrc
5
.envrc
@@ -1,4 +1 @@
|
|||||||
if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
|
use nix
|
||||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
|
|
||||||
fi
|
|
||||||
use flake
|
|
||||||
|
|||||||
1
.git-blame-ignore-revs
Normal file
1
.git-blame-ignore-revs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
14e241db37c4080bc0bd87363cf7a57ef8379f46
|
||||||
152
.github/workflows/CI.yml
vendored
152
.github/workflows/CI.yml
vendored
@@ -1,151 +1,18 @@
|
|||||||
name: CI
|
name: Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '*'
|
- main
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
deployment:
|
||||||
## Build Back ##
|
|
||||||
|
|
||||||
Build_Back:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
if: github.event.pull_request.merged == true
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./back
|
|
||||||
environment: Staging
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build Docker
|
|
||||||
run: docker build -t testback .
|
|
||||||
|
|
||||||
## Build App ##
|
|
||||||
|
|
||||||
Build_Front:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./front
|
|
||||||
environment: Staging
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install Yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install
|
|
||||||
|
|
||||||
- name: Type Check
|
|
||||||
run: yarn tsc
|
|
||||||
- name: Check Prettier
|
|
||||||
run: yarn pretty:check .
|
|
||||||
- name: Run Linter
|
|
||||||
run: yarn lint
|
|
||||||
|
|
||||||
- name: 🏗 Setup Expo
|
|
||||||
uses: expo/expo-github-action@v7
|
|
||||||
with:
|
|
||||||
expo-version: latest
|
|
||||||
eas-version: 3.3.1
|
|
||||||
token: ${{ secrets.EXPO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build Android APK
|
|
||||||
run: |
|
|
||||||
eas build -p android --profile production --local --non-interactive
|
|
||||||
mv *.apk chromacase.apk
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: chromacase.apk
|
|
||||||
path: front/
|
|
||||||
|
|
||||||
## Test Backend ##
|
|
||||||
|
|
||||||
Test_Back:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
needs: [ Build_Back ]
|
|
||||||
environment: Staging
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Copy env file to github secret env file
|
|
||||||
run: |
|
|
||||||
touch .env
|
|
||||||
echo "POSTGRES_USER=user" >> .env
|
|
||||||
echo "POSTGRES_PASSWORD=eip" >> .env
|
|
||||||
echo "POSTGRES_NAME=chromacase" >> .env
|
|
||||||
echo "POSTGRES_HOST=db" >> .env
|
|
||||||
echo "DATABASE_URL=postgresql://user:eip@db:5432/chromacase" >> .env
|
|
||||||
echo "JWT_SECRET=wow" >> .env
|
|
||||||
echo "POSTGRES_DB=chromacase" >> .env
|
|
||||||
echo "API_URL=http://localhost:80/api" >> .env
|
|
||||||
|
|
||||||
- name: Start the service
|
|
||||||
run: docker-compose up -d back db
|
|
||||||
|
|
||||||
- name: Perform healthchecks
|
|
||||||
run: |
|
|
||||||
docker-compose ps -a
|
|
||||||
wget --retry-connrefused http://localhost:3000 # /healthcheck
|
|
||||||
|
|
||||||
- name: Run scorometer tests
|
|
||||||
run: |
|
|
||||||
pip install -r scorometer/requirements.txt
|
|
||||||
cd scorometer/tests && ./runner.sh
|
|
||||||
|
|
||||||
- name: Run robot tests
|
|
||||||
run: |
|
|
||||||
pip install -r back/test/robot/requirements.txt
|
|
||||||
robot -d out back/test/robot/
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: results
|
|
||||||
path: out
|
|
||||||
|
|
||||||
- name: Write results to Pull Request and Summary
|
|
||||||
if: always() && github.event_name == 'pull_request'
|
|
||||||
uses: joonvena/robotframework-reporter-action@v2.1
|
|
||||||
with:
|
|
||||||
report_path: out/
|
|
||||||
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
only_summary: false
|
|
||||||
|
|
||||||
- name: Write results to Summary
|
|
||||||
if: always() && github.event_name != 'pull_request'
|
|
||||||
uses: joonvena/robotframework-reporter-action@v2.1
|
|
||||||
with:
|
|
||||||
report_path: out/
|
|
||||||
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
only_summary: true
|
|
||||||
|
|
||||||
- name: Remove .env && stop the service
|
|
||||||
run: docker-compose down && rm .env
|
|
||||||
|
|
||||||
## Test App ##
|
|
||||||
|
|
||||||
## Deployement ##
|
|
||||||
|
|
||||||
Deployement_Docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
needs: [ Test_Back ]
|
|
||||||
environment: Production
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -190,6 +57,7 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
API_URL=${{secrets.API_URL}}
|
API_URL=${{secrets.API_URL}}
|
||||||
SCORO_URL=${{secrets.SCORO_URL}}
|
SCORO_URL=${{secrets.SCORO_URL}}
|
||||||
|
|
||||||
- name: Docker meta scorometer
|
- name: Docker meta scorometer
|
||||||
id: meta_scorometer
|
id: meta_scorometer
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
|
|||||||
101
.github/workflows/back.yml
vendored
Normal file
101
.github/workflows/back.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
name: "Back"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Required permissions
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
# Set job outputs to values from filter step
|
||||||
|
outputs:
|
||||||
|
back: ${{ steps.filter.outputs.back }}
|
||||||
|
front: ${{ steps.filter.outputs.front }}
|
||||||
|
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||||
|
steps:
|
||||||
|
# For pull requests it's not necessary to checkout the code
|
||||||
|
- uses: dorny/paths-filter@v2
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
back:
|
||||||
|
- 'back/**'
|
||||||
|
- '.github/workflows/back.yml'
|
||||||
|
front:
|
||||||
|
- 'front/**'
|
||||||
|
- '.github/workflows/front.yml'
|
||||||
|
scorometer:
|
||||||
|
- 'scorometer/**'
|
||||||
|
- '.github/workflows/scoro.yml'
|
||||||
|
back_build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./back
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build Docker
|
||||||
|
run: docker build -t testback .
|
||||||
|
|
||||||
|
|
||||||
|
back_test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
needs: [ back_build ]
|
||||||
|
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Copy env file to github secret env file
|
||||||
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Build and start the service
|
||||||
|
run: docker-compose up -d meilisearch back db
|
||||||
|
|
||||||
|
- name: Perform healthchecks
|
||||||
|
run: |
|
||||||
|
docker-compose ps -a
|
||||||
|
docker-compose logs
|
||||||
|
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
|
||||||
|
|
||||||
|
- name: Run robot tests
|
||||||
|
run: |
|
||||||
|
export API_KEY_ROBOT=ROBOTO
|
||||||
|
pip install -r back/test/robot/requirements.txt
|
||||||
|
robot -d out back/test/robot/
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: results
|
||||||
|
path: out
|
||||||
|
|
||||||
|
- name: Write results to Pull Request and Summary
|
||||||
|
if: always() && github.event_name == 'pull_request'
|
||||||
|
uses: joonvena/robotframework-reporter-action@v2.1
|
||||||
|
with:
|
||||||
|
report_path: out/
|
||||||
|
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
only_summary: false
|
||||||
|
|
||||||
|
- name: Write results to Summary
|
||||||
|
if: always() && github.event_name != 'pull_request'
|
||||||
|
uses: joonvena/robotframework-reporter-action@v2.1
|
||||||
|
with:
|
||||||
|
report_path: out/
|
||||||
|
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
only_summary: true
|
||||||
|
|
||||||
|
- name: stop the service
|
||||||
|
run: docker-compose down
|
||||||
98
.github/workflows/front.yml
vendored
Normal file
98
.github/workflows/front.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: "Front"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Required permissions
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
# Set job outputs to values from filter step
|
||||||
|
outputs:
|
||||||
|
back: ${{ steps.filter.outputs.back }}
|
||||||
|
front: ${{ steps.filter.outputs.front }}
|
||||||
|
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||||
|
steps:
|
||||||
|
# For pull requests it's not necessary to checkout the code
|
||||||
|
- uses: dorny/paths-filter@v2
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
back:
|
||||||
|
- 'back/**'
|
||||||
|
- '.github/workflows/back.yml'
|
||||||
|
front:
|
||||||
|
- 'front/**'
|
||||||
|
- '.github/workflows/front.yml'
|
||||||
|
scorometer:
|
||||||
|
- 'scorometer/**'
|
||||||
|
- '.github/workflows/scoro.yml'
|
||||||
|
front_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./front
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'yarn'
|
||||||
|
cache-dependency-path: front/yarn.lock
|
||||||
|
- run: yarn install --frozen-lockfile
|
||||||
|
- name: type check
|
||||||
|
run: yarn tsc
|
||||||
|
- name: prettier
|
||||||
|
run: yarn pretty:check .
|
||||||
|
- name: eslint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
front_build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./front
|
||||||
|
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||||
|
needs: [ front_check ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'yarn'
|
||||||
|
cache-dependency-path: front/yarn.lock
|
||||||
|
- run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: 🏗 Setup Expo
|
||||||
|
uses: expo/expo-github-action@v8
|
||||||
|
with:
|
||||||
|
expo-version: latest
|
||||||
|
eas-version: latest
|
||||||
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build Web App
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: ./front
|
||||||
|
push: false
|
||||||
|
tags: ${{steps.meta_front.outputs.tags}}
|
||||||
|
build-args: |
|
||||||
|
API_URL=${{secrets.API_URL}}
|
||||||
|
SCORO_URL=${{secrets.SCORO_URL}}
|
||||||
|
|
||||||
|
- name: Build Android APK
|
||||||
|
run: |
|
||||||
|
eas build -p android --profile production --local --non-interactive
|
||||||
|
mv *.apk chromacase.apk
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: chromacase.apk
|
||||||
|
path: front/
|
||||||
63
.github/workflows/scoro.yml
vendored
Normal file
63
.github/workflows/scoro.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: "Scoro"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Required permissions
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
# Set job outputs to values from filter step
|
||||||
|
outputs:
|
||||||
|
back: ${{ steps.filter.outputs.back }}
|
||||||
|
front: ${{ steps.filter.outputs.front }}
|
||||||
|
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||||
|
steps:
|
||||||
|
# For pull requests it's not necessary to checkout the code
|
||||||
|
- uses: dorny/paths-filter@v2
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
back:
|
||||||
|
- 'back/**'
|
||||||
|
- '.github/workflows/back.yml'
|
||||||
|
front:
|
||||||
|
- 'front/**'
|
||||||
|
- '.github/workflows/front.yml'
|
||||||
|
scorometer:
|
||||||
|
- 'scorometer/**'
|
||||||
|
- '.github/workflows/scoro.yml'
|
||||||
|
scoro_test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.scorometer == 'true' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Copy env file to github secret env file
|
||||||
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Build and start the service
|
||||||
|
run: docker-compose up -d meilisearch back db
|
||||||
|
|
||||||
|
- name: Perform healthchecks
|
||||||
|
run: |
|
||||||
|
docker-compose ps -a
|
||||||
|
docker-compose logs
|
||||||
|
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
|
||||||
|
|
||||||
|
- name: Run scorometer tests
|
||||||
|
run: |
|
||||||
|
export API_KEY_SCORO_TEST=SCOROTEST
|
||||||
|
export API_KEY_SCORO=SCORO
|
||||||
|
pip install -r scorometer/requirements.txt
|
||||||
|
cd scorometer/tests && ./runner.sh
|
||||||
|
|
||||||
|
- name: stop the service
|
||||||
|
run: docker-compose down
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,3 +13,7 @@ log.html
|
|||||||
node_modules/
|
node_modules/
|
||||||
./front/coverage
|
./front/coverage
|
||||||
.venv
|
.venv
|
||||||
|
.data
|
||||||
|
.DS_Store
|
||||||
|
_gen
|
||||||
|
venv
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -1,9 +1,39 @@
|
|||||||
# 
|
# 
|
||||||
|
|
||||||
La principale raison pour laquelle on arrête de jouer d'un instrument est la perte de motivation. C'est un apprentissage long et vraiment demandant. ChromaCase propose d'accompagner les joueurs de piano grâce à une application mobile avec une expérience personnalisée. Celle-ci, générée par une IA, cible les goûts et identifie les difficultés du joueur.
|
La principale raison pour laquelle on arrête de jouer d'un instrument est la perte de motivation. C'est un apprentissage long et vraiment demandant. ChromaCase propose d'accompagner les joueurs de piano grâce à une application mobile avec une expérience personnalisée. Celle-ci, générée par une IA, cible les goûts et identifie les difficultés du joueur.
|
||||||
|
|
||||||
Ça vous interesse? Rendez-vous sur notre [site](https://chromacase.studio/) pour prendre contact
|
Ça vous interesse? Rendez-vous sur notre [site](http://eip.epitech.eu/2024/chromacase) pour prendre contact
|
||||||
|
|
||||||
## Structure du Projet
|
## Comment lancer le projet
|
||||||
|
|
||||||

|
Pensez à remplir un `.env` (à la racine du projet), en se basant sur le `.env.example`.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Liens Utiles
|
||||||
|
|
||||||
|
- Site de Production: [Lien](http://chroma.octohub.app/)
|
||||||
|
- Site du Nightly: [Lien](http://nightly.chroma.octohub.app/)
|
||||||
|
- Site vitrine: [Lien](http://eip.epitech.eu/2024/chromacase)
|
||||||
|
- Documentation: [Github](https://github.com/Chroma-Case/DAteX)
|
||||||
|
|
||||||
|
## Membres du Projet
|
||||||
|
|
||||||
|
| Nom | Role | Contact |
|
||||||
|
|--------------------------|--------------------------------------|----------------------------------------------------|
|
||||||
|
| Zoé Roux | CEO, Responsable Back-end | [GitHub](https://github.com/zoriya) |
|
||||||
|
| Clément Le-Bihan | CTO, Responsable Front-end | [GitHub](https://github.com/Octopus773) |
|
||||||
|
| Arthur Jamet | Manager, Développeur Front-end | [GitHub](https://github.com/Arthi-chaud) |
|
||||||
|
| Louis Auzuret | Développeur Back-end, Responsable CI | [Github](https://github.com/GitBluub) |
|
||||||
|
| Aumaury Danis-Cousandier | Développeur Front-end | [Github](https://github.com/AmauryDanisCousandier) |
|
||||||
|
| Mathys Paul | Développeur Front-end, Designer | [GitHub](https://github.com/mathysPaul) |
|
||||||
|
|||||||
19
assets/create_melodies.sh
Executable file
19
assets/create_melodies.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Iterate through subfolders
|
||||||
|
find . -type d | while read -r dir; do
|
||||||
|
# Check if .midi file exists in the subfolder
|
||||||
|
midi_file=$(find "$dir" -maxdepth 1 -type f -name '*.midi' | head -n 1)
|
||||||
|
|
||||||
|
if [ -n "$midi_file" ]; then
|
||||||
|
# Create output file name (melody.mp3) in the same subfolder
|
||||||
|
output_file="${dir}/melody.mp3"
|
||||||
|
|
||||||
|
# Run the given command
|
||||||
|
#timidity "$midi_file" -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 64k "$output_file"
|
||||||
|
fluidsynth -a alsa -T raw -F - "$midi_file" | ffmpeg -f s32le -i - "$output_file"
|
||||||
|
|
||||||
|
echo "Converted: $midi_file to $output_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
BIN
assets/graphical/banner.png
Normal file
BIN
assets/graphical/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 597 KiB |
BIN
assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3
Normal file
BIN
assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Canon in D (easy)/melody.mp3
Normal file
BIN
assets/musics/Canon in D (easy)/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/musics/French National Anthem La Marseillaise/melody.mp3
Normal file
BIN
assets/musics/French National Anthem La Marseillaise/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Jesus Alegria dos Homens/melody.mp3
Normal file
BIN
assets/musics/Jesus Alegria dos Homens/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Liebestraum (easy)/melody.mp3
Normal file
BIN
assets/musics/Liebestraum (easy)/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Mary, Did You Know/melody.mp3
Normal file
BIN
assets/musics/Mary, Did You Know/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/SCORO_TEST/melody.mp3
Normal file
BIN
assets/musics/SCORO_TEST/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Sarabande - William Gillock/melody.mp3
Normal file
BIN
assets/musics/Sarabande - William Gillock/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Short/melody.mp3
Normal file
BIN
assets/musics/Short/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Silent Night/melody.mp3
Normal file
BIN
assets/musics/Silent Night/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Twinkle Twinkle Little Star/melody.mp3
Normal file
BIN
assets/musics/Twinkle Twinkle Little Star/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,12 +1,17 @@
|
|||||||
#!/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import glob
|
import glob
|
||||||
|
from mido import MidiFile
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
url = os.environ.get("API_URL")
|
url = os.environ.get("API_URL")
|
||||||
|
api_key = os.environ.get("API_KEY_POPULATE")
|
||||||
|
auth_headers = {}
|
||||||
|
auth_headers["Authorization"] = f"API Key {api_key}"
|
||||||
|
|
||||||
|
|
||||||
def getOrCreateAlbum(name, artistId):
|
def getOrCreateAlbum(name, artistId):
|
||||||
if not name:
|
if not name:
|
||||||
@@ -14,27 +19,27 @@ def getOrCreateAlbum(name, artistId):
|
|||||||
res = requests.post(f"{url}/album", json={
|
res = requests.post(f"{url}/album", json={
|
||||||
"name": name,
|
"name": name,
|
||||||
"artist": artistId,
|
"artist": artistId,
|
||||||
})
|
},headers=auth_headers)
|
||||||
out = res.json()
|
out = res.json()
|
||||||
print(out)
|
print(out)
|
||||||
return out["id"]
|
return out["id"]
|
||||||
|
|
||||||
def getOrCreateGenre(names):
|
def getOrCreateGenre(names):
|
||||||
ids = []
|
ids = []
|
||||||
for name in names.split(","):
|
for name in names.split(","):
|
||||||
res = requests.post(f"{url}/genre", json={
|
res = requests.post(f"{url}/genre", json={
|
||||||
"name": name,
|
"name": name,
|
||||||
})
|
},headers=auth_headers)
|
||||||
out = res.json()
|
out = res.json()
|
||||||
print(out)
|
print(out)
|
||||||
ids += [out["id"]]
|
ids += [out["id"]]
|
||||||
#TODO handle multiple genres
|
#TODO handle multiple genres
|
||||||
return ids[0]
|
return ids[0]
|
||||||
|
|
||||||
def getOrCreateArtist(name):
|
def getOrCreateArtist(name):
|
||||||
res = requests.post(f"{url}/artist", json={
|
res = requests.post(f"{url}/artist", json={
|
||||||
"name": name,
|
"name": name,
|
||||||
})
|
},headers=auth_headers)
|
||||||
out = res.json()
|
out = res.json()
|
||||||
print(out)
|
print(out)
|
||||||
return out["id"]
|
return out["id"]
|
||||||
@@ -42,10 +47,13 @@ def getOrCreateArtist(name):
|
|||||||
def populateFile(path, midi, mxl):
|
def populateFile(path, midi, mxl):
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.read(path)
|
config.read(path)
|
||||||
|
mid = MidiFile(midi)
|
||||||
metadata = config["Metadata"];
|
metadata = config["Metadata"];
|
||||||
difficulties = dict(config["Difficulties"])
|
difficulties = dict(config["Difficulties"])
|
||||||
|
difficulties["length"] = round((mid.length), 2)
|
||||||
artistId = getOrCreateArtist(metadata["Artist"])
|
artistId = getOrCreateArtist(metadata["Artist"])
|
||||||
print(f"Populating {metadata['Name']}")
|
print(f"Populating {metadata['Name']}")
|
||||||
|
print(auth_headers)
|
||||||
res = requests.post(f"{url}/song", json={
|
res = requests.post(f"{url}/song", json={
|
||||||
"name": metadata["Name"],
|
"name": metadata["Name"],
|
||||||
"midiPath": f"/assets/{midi}",
|
"midiPath": f"/assets/{midi}",
|
||||||
@@ -55,7 +63,7 @@ def populateFile(path, midi, mxl):
|
|||||||
"album": getOrCreateAlbum(metadata["Album"], artistId),
|
"album": getOrCreateAlbum(metadata["Album"], artistId),
|
||||||
"genre": getOrCreateGenre(metadata["Genre"]),
|
"genre": getOrCreateGenre(metadata["Genre"]),
|
||||||
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
|
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
|
||||||
})
|
}, headers=auth_headers)
|
||||||
print(res.json())
|
print(res.json())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
assets/requirements.txt
Normal file
2
assets/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mido
|
||||||
|
requests
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": false,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all"
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,4 @@ RUN npm install --frozen-lockfile
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
CMD npx prisma migrate dev; npm run start:prod
|
CMD npx prisma migrate deploy; npm run start:prod
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
FROM node:17
|
FROM node:18.10.0
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||||
|
|||||||
20883
back/package-lock.json
generated
20883
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@
|
|||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch --preserveWatchOutput",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch --preserveWatchOutput",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
@@ -21,57 +21,74 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^8.0.0",
|
"@nestjs-modules/mailer": "^1.9.1",
|
||||||
"@nestjs/config": "^2.1.0",
|
"@nestjs/common": "^10.1.0",
|
||||||
"@nestjs/core": "^8.0.0",
|
"@nestjs/config": "^3.0.0",
|
||||||
"@nestjs/jwt": "^8.0.1",
|
"@nestjs/core": "^10.1.0",
|
||||||
|
"@nestjs/jwt": "^10.1.0",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/passport": "^8.2.2",
|
"@nestjs/passport": "^10.0.0",
|
||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^10.1.0",
|
||||||
"@nestjs/swagger": "^5.2.1",
|
"@nestjs/swagger": "^7.1.2",
|
||||||
"@prisma/client": "^4.4.0",
|
"@prisma/client": "^5.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/passport": "^1.0.9",
|
"@types/passport": "^1.0.12",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"canvas": "^2.11.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.14.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"cross-blob": "^3.0.2",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"json-logger-service": "^9.0.1",
|
||||||
|
"meilisearch": "^0.35.0",
|
||||||
|
"node-fetch": "^2.6.12",
|
||||||
|
"nodemailer": "^6.9.5",
|
||||||
|
"opensheetmusicdisplay": "^1.8.4",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
"passport-headerapikey": "^1.2.2",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"prisma-class-generator": "^0.2.7",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^5.0.1",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.8.1",
|
||||||
"swagger-ui-express": "^4.5.0"
|
"swagger-ui-express": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.0.0",
|
"@nestjs/cli": "^10.1.10",
|
||||||
"@nestjs/schematics": "^8.0.0",
|
"@nestjs/schematics": "^10.0.1",
|
||||||
"@nestjs/testing": "^8.0.0",
|
"@nestjs/testing": "^10.1.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "27.4.1",
|
"@types/jest": "29.5.3",
|
||||||
"@types/node": "^16.0.0",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/node": "^20.4.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@types/nodemailer": "^6.4.9",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@types/passport-google-oauth20": "^2.0.11",
|
||||||
"eslint": "^8.0.1",
|
"@types/supertest": "^2.0.12",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"@typescript-eslint/parser": "^6.1.0",
|
||||||
"jest": "^27.2.5",
|
"eslint": "^8.45.0",
|
||||||
"prettier": "^2.3.2",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"prisma": "^4.4.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"source-map-support": "^0.5.20",
|
"jest": "^29.6.1",
|
||||||
"supertest": "^6.1.3",
|
"prettier": "^3.0.0",
|
||||||
"ts-jest": "^27.0.3",
|
"prisma": "^5.0.0",
|
||||||
"ts-loader": "^9.2.3",
|
"source-map-support": "^0.5.21",
|
||||||
"ts-node": "^10.0.0",
|
"supertest": "^6.3.3",
|
||||||
"tsconfig-paths": "^3.10.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.3.5"
|
"ts-loader": "^9.4.4",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"js",
|
"js",
|
||||||
"json",
|
"json",
|
||||||
"ts"
|
"ts",
|
||||||
|
"mjs"
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
|||||||
12
back/prisma/migrations/20230621090510_google/migration.sql
Normal file
12
back/prisma/migrations/20230621090510_google/migration.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[googleID]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "googleID" TEXT,
|
||||||
|
ALTER COLUMN "password" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_googleID_key" ON "User"("googleID");
|
||||||
2
back/prisma/migrations/20230907141258_/migration.sql
Normal file
2
back/prisma/migrations/20230907141258_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LikedSongs" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"songId" INTEGER NOT NULL,
|
||||||
|
"addedDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "LikedSongs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LikedSongs" ADD CONSTRAINT "LikedSongs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LikedSongs" ADD CONSTRAINT "LikedSongs_songId_fkey" FOREIGN KEY ("songId") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
8
back/prisma/migrations/20230920151856_/migration.sql
Normal file
8
back/prisma/migrations/20230920151856_/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
2
back/prisma/migrations/20230921103156_/migration.sql
Normal file
2
back/prisma/migrations/20230921103156_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "totalScore" INTEGER NOT NULL DEFAULT 0;
|
||||||
@@ -4,6 +4,12 @@ generator client {
|
|||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generator prismaClassGenerator {
|
||||||
|
provider = "prisma-class-generator"
|
||||||
|
dryRun = false
|
||||||
|
separateRelationFields = true
|
||||||
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
@@ -12,14 +18,27 @@ datasource db {
|
|||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
password String?
|
||||||
email String
|
email String? @unique
|
||||||
|
emailVerified Boolean @default(false)
|
||||||
|
googleID String? @unique
|
||||||
isGuest Boolean @default(false)
|
isGuest Boolean @default(false)
|
||||||
partyPlayed Int @default(0)
|
partyPlayed Int @default(0)
|
||||||
|
totalScore Int @default(0)
|
||||||
LessonHistory LessonHistory[]
|
LessonHistory LessonHistory[]
|
||||||
SongHistory SongHistory[]
|
SongHistory SongHistory[]
|
||||||
searchHistory SearchHistory[]
|
searchHistory SearchHistory[]
|
||||||
settings UserSettings?
|
settings UserSettings?
|
||||||
|
likedSongs LikedSongs[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model LikedSongs {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId Int
|
||||||
|
song Song @relation(fields: [songId], references: [id], onDelete: Cascade)
|
||||||
|
songId Int
|
||||||
|
addedDate DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserSettings {
|
model UserSettings {
|
||||||
@@ -59,6 +78,7 @@ model Song {
|
|||||||
genre Genre? @relation(fields: [genreId], references: [id])
|
genre Genre? @relation(fields: [genreId], references: [id])
|
||||||
difficulties Json
|
difficulties Json
|
||||||
SongHistory SongHistory[]
|
SongHistory SongHistory[]
|
||||||
|
likedByUsers LikedSongs[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model SongHistory {
|
model SongHistory {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
|
||||||
Body,
|
Body,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Controller,
|
Controller,
|
||||||
@@ -12,23 +11,36 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
} from '@nestjs/common';
|
UseGuards,
|
||||||
import { Plage } from 'src/models/plage';
|
} from "@nestjs/common";
|
||||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { AlbumService } from './album.service';
|
import { CreateAlbumDto } from "./dto/create-album.dto";
|
||||||
import { Request } from 'express';
|
import { AlbumService } from "./album.service";
|
||||||
import { Prisma, Album } from '@prisma/client';
|
import { Request } from "express";
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { Prisma, Album } from "@prisma/client";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
|
import { Album as _Album } from "src/_gen/prisma-class/album";
|
||||||
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
@Controller('album')
|
@Controller("album")
|
||||||
@ApiTags('album')
|
@ApiTags("album")
|
||||||
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class AlbumController {
|
export class AlbumController {
|
||||||
static filterableFields: string[] = ['+id', 'name', '+artistId'];
|
static filterableFields: string[] = ["+id", "name", "+artistId"];
|
||||||
|
static includableFields: IncludeMap<Prisma.AlbumInclude> = {
|
||||||
|
artist: true,
|
||||||
|
Song: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private readonly albumService: AlbumService) {}
|
constructor(private readonly albumService: AlbumService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Register a new album, should not be used by frontend",
|
||||||
|
})
|
||||||
async create(@Body() createAlbumDto: CreateAlbumDto) {
|
async create(@Body() createAlbumDto: CreateAlbumDto) {
|
||||||
try {
|
try {
|
||||||
return await this.albumService.createAlbum({
|
return await this.albumService.createAlbum({
|
||||||
@@ -44,36 +56,50 @@ export class AlbumController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
@ApiOperation({ description: "Delete an album by id" })
|
||||||
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.albumService.deleteAlbum({ id });
|
return await this.albumService.deleteAlbum({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOkResponsePlaginated(_Album)
|
||||||
|
@ApiOperation({ description: "Get all albums paginated" })
|
||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(AlbumController.filterableFields)
|
@FilterQuery(AlbumController.filterableFields)
|
||||||
where: Prisma.AlbumWhereInput,
|
where: Prisma.AlbumWhereInput,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("include") include: string,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Album>> {
|
): Promise<Plage<Album>> {
|
||||||
const ret = await this.albumService.albums({
|
const ret = await this.albumService.albums({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where,
|
where,
|
||||||
|
include: mapInclude(include, req, AlbumController.includableFields),
|
||||||
});
|
});
|
||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
@ApiOperation({ description: "Get an album by id" })
|
||||||
const res = await this.albumService.album({ id });
|
@ApiOkResponse({ type: _Album })
|
||||||
|
async findOne(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Query("include") include: string,
|
||||||
|
@Param("id", ParseIntPipe) id: number,
|
||||||
|
) {
|
||||||
|
const res = await this.albumService.album(
|
||||||
|
{ id },
|
||||||
|
mapInclude(include, req, AlbumController.includableFields),
|
||||||
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Album not found');
|
if (res === null) throw new NotFoundException("Album not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { AlbumController } from './album.controller';
|
import { AlbumController } from "./album.controller";
|
||||||
import { AlbumService } from './album.service';
|
import { AlbumService } from "./album.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, Album } from '@prisma/client';
|
import { Prisma, Album } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlbumService {
|
export class AlbumService {
|
||||||
@@ -14,9 +14,11 @@ export class AlbumService {
|
|||||||
|
|
||||||
async album(
|
async album(
|
||||||
albumWhereUniqueInput: Prisma.AlbumWhereUniqueInput,
|
albumWhereUniqueInput: Prisma.AlbumWhereUniqueInput,
|
||||||
|
include?: Prisma.AlbumInclude,
|
||||||
): Promise<Album | null> {
|
): Promise<Album | null> {
|
||||||
return this.prisma.album.findUnique({
|
return this.prisma.album.findUnique({
|
||||||
where: albumWhereUniqueInput,
|
where: albumWhereUniqueInput,
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,14 +28,16 @@ export class AlbumService {
|
|||||||
cursor?: Prisma.AlbumWhereUniqueInput;
|
cursor?: Prisma.AlbumWhereUniqueInput;
|
||||||
where?: Prisma.AlbumWhereInput;
|
where?: Prisma.AlbumWhereInput;
|
||||||
orderBy?: Prisma.AlbumOrderByWithRelationInput;
|
orderBy?: Prisma.AlbumOrderByWithRelationInput;
|
||||||
|
include?: Prisma.AlbumInclude;
|
||||||
}): Promise<Album[]> {
|
}): Promise<Album[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { skip, take, cursor, where, orderBy, include } = params;
|
||||||
return this.prisma.album.findMany({
|
return this.prisma.album.findMany({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class CreateAlbumDto {
|
export class CreateAlbumDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from "./app.controller";
|
||||||
import { AppService } from './app.service';
|
import { AppService } from "./app.service";
|
||||||
|
|
||||||
describe('AppController', () => {
|
describe("AppController", () => {
|
||||||
let appController: AppController;
|
let appController: AppController;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -14,9 +14,9 @@ describe('AppController', () => {
|
|||||||
appController = app.get<AppController>(AppController);
|
appController = app.get<AppController>(AppController);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('root', () => {
|
describe("root", () => {
|
||||||
it('should return "Hello World!"', () => {
|
it('should return "Hello World!"', () => {
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
expect(appController.getHello()).toBe("Hello World!");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from "@nestjs/common";
|
||||||
import { AppService } from './app.service';
|
import { AppService } from "./app.service";
|
||||||
|
import { ApiOkResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Return a hello world message, used as a health route",
|
||||||
|
})
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from "./app.controller";
|
||||||
import { AppService } from './app.service';
|
import { AppService } from "./app.service";
|
||||||
import { PrismaService } from './prisma/prisma.service';
|
import { PrismaService } from "./prisma/prisma.service";
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from "./users/users.module";
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from "./prisma/prisma.module";
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from "./auth/auth.module";
|
||||||
import { SongModule } from './song/song.module';
|
import { SongModule } from "./song/song.module";
|
||||||
import { LessonModule } from './lesson/lesson.module';
|
import { LessonModule } from "./lesson/lesson.module";
|
||||||
import { SettingsModule } from './settings/settings.module';
|
import { SettingsModule } from "./settings/settings.module";
|
||||||
import { ArtistService } from './artist/artist.service';
|
import { ArtistService } from "./artist/artist.service";
|
||||||
import { GenreModule } from './genre/genre.module';
|
import { GenreModule } from "./genre/genre.module";
|
||||||
import { ArtistModule } from './artist/artist.module';
|
import { ArtistModule } from "./artist/artist.module";
|
||||||
import { AlbumModule } from './album/album.module';
|
import { AlbumModule } from "./album/album.module";
|
||||||
import { SearchModule } from './search/search.module';
|
import { SearchModule } from "./search/search.module";
|
||||||
import { HistoryModule } from './history/history.module';
|
import { HistoryModule } from "./history/history.module";
|
||||||
|
import { MailerModule } from "@nestjs-modules/mailer";
|
||||||
|
import { ScoresModule } from "./scores/scores.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -28,6 +30,13 @@ import { HistoryModule } from './history/history.module';
|
|||||||
SearchModule,
|
SearchModule,
|
||||||
SettingsModule,
|
SettingsModule,
|
||||||
HistoryModule,
|
HistoryModule,
|
||||||
|
ScoresModule,
|
||||||
|
MailerModule.forRoot({
|
||||||
|
transport: process.env.SMTP_TRANSPORT,
|
||||||
|
defaults: {
|
||||||
|
from: process.env.MAIL_AUTHOR,
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, PrismaService, ArtistService],
|
providers: [AppService, PrismaService, ArtistService],
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return 'Hello World!';
|
return "Hello World!";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
|
||||||
Body,
|
Body,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Controller,
|
Controller,
|
||||||
@@ -14,24 +13,43 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
} from '@nestjs/common';
|
UseGuards,
|
||||||
import { Plage } from 'src/models/plage';
|
} from "@nestjs/common";
|
||||||
import { CreateArtistDto } from './dto/create-artist.dto';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { Request } from 'express';
|
import { CreateArtistDto } from "./dto/create-artist.dto";
|
||||||
import { ArtistService } from './artist.service';
|
import { Request } from "express";
|
||||||
import { Prisma, Artist } from '@prisma/client';
|
import { ArtistService } from "./artist.service";
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { Prisma, Artist } from "@prisma/client";
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import {
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { createReadStream, existsSync } from "fs";
|
||||||
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
|
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||||
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
|
import { Public } from "src/auth/public";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
@Controller('artist')
|
@Controller("artist")
|
||||||
@ApiTags('artist')
|
@ApiTags("artist")
|
||||||
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class ArtistController {
|
export class ArtistController {
|
||||||
static filterableFields = ['+id', 'name'];
|
static filterableFields = ["+id", "name"];
|
||||||
|
static includableFields: IncludeMap<Prisma.ArtistInclude> = {
|
||||||
|
Song: true,
|
||||||
|
Album: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private readonly service: ArtistService) {}
|
constructor(private readonly service: ArtistService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Register a new artist, should not be used by frontend",
|
||||||
|
})
|
||||||
async create(@Body() dto: CreateArtistDto) {
|
async create(@Body() dto: CreateArtistDto) {
|
||||||
try {
|
try {
|
||||||
return await this.service.create(dto);
|
return await this.service.create(dto);
|
||||||
@@ -40,22 +58,26 @@ export class ArtistController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
@ApiOperation({ description: "Delete an artist by id" })
|
||||||
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.service.delete({ id });
|
return await this.service.delete({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/illustration')
|
@Get(":id/illustration")
|
||||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
@ApiOperation({ description: "Get an artist's illustration" })
|
||||||
|
@ApiNotFoundResponse({ description: "Artist or illustration not found" })
|
||||||
|
@Public()
|
||||||
|
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||||
const artist = await this.service.get({ id });
|
const artist = await this.service.get({ id });
|
||||||
if (!artist) throw new NotFoundException('Artist not found');
|
if (!artist) throw new NotFoundException("Artist not found");
|
||||||
const path = `/assets/artists/${artist.name}/illustration.png`;
|
const path = `/assets/artists/${artist.name}/illustration.png`;
|
||||||
if (!existsSync(path))
|
if (!existsSync(path))
|
||||||
throw new NotFoundException('Illustration not found');
|
throw new NotFoundException("Illustration not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(path);
|
const file = createReadStream(path);
|
||||||
@@ -66,26 +88,39 @@ export class ArtistController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOperation({ description: "Get all artists paginated" })
|
||||||
|
@ApiOkResponsePlaginated(_Artist)
|
||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(ArtistController.filterableFields)
|
@FilterQuery(ArtistController.filterableFields)
|
||||||
where: Prisma.ArtistWhereInput,
|
where: Prisma.ArtistWhereInput,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("include") include: string,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Artist>> {
|
): Promise<Plage<Artist>> {
|
||||||
const ret = await this.service.list({
|
const ret = await this.service.list({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where,
|
where,
|
||||||
|
include: mapInclude(include, req, ArtistController.includableFields),
|
||||||
});
|
});
|
||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
@ApiOperation({ description: "Get an artist by id" })
|
||||||
const res = await this.service.get({ id });
|
@ApiOkResponse({ type: _Artist })
|
||||||
|
async findOne(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Query("include") include: string,
|
||||||
|
@Param("id", ParseIntPipe) id: number,
|
||||||
|
) {
|
||||||
|
const res = await this.service.get(
|
||||||
|
{ id },
|
||||||
|
mapInclude(include, req, ArtistController.includableFields),
|
||||||
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Artist not found');
|
if (res === null) throw new NotFoundException("Artist not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { ArtistController } from './artist.controller';
|
import { ArtistController } from "./artist.controller";
|
||||||
import { ArtistService } from './artist.service';
|
import { ArtistService } from "./artist.service";
|
||||||
|
import { SearchModule } from "src/search/search.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, SearchModule],
|
||||||
controllers: [ArtistController],
|
controllers: [ArtistController],
|
||||||
providers: [ArtistService],
|
providers: [ArtistService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, Artist } from '@prisma/client';
|
import { Prisma, Artist } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { MeiliService } from "src/search/meilisearch.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ArtistService {
|
export class ArtistService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private search: MeiliService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
|
async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
|
||||||
return this.prisma.artist.create({
|
const ret = await this.prisma.artist.create({
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
await this.search.index("artists").addDocuments([ret]);
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(where: Prisma.ArtistWhereUniqueInput): Promise<Artist | null> {
|
async get(
|
||||||
|
where: Prisma.ArtistWhereUniqueInput,
|
||||||
|
include?: Prisma.ArtistInclude,
|
||||||
|
): Promise<Artist | null> {
|
||||||
return this.prisma.artist.findUnique({
|
return this.prisma.artist.findUnique({
|
||||||
where,
|
where,
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,20 +34,24 @@ export class ArtistService {
|
|||||||
cursor?: Prisma.ArtistWhereUniqueInput;
|
cursor?: Prisma.ArtistWhereUniqueInput;
|
||||||
where?: Prisma.ArtistWhereInput;
|
where?: Prisma.ArtistWhereInput;
|
||||||
orderBy?: Prisma.ArtistOrderByWithRelationInput;
|
orderBy?: Prisma.ArtistOrderByWithRelationInput;
|
||||||
|
include?: Prisma.ArtistInclude;
|
||||||
}): Promise<Artist[]> {
|
}): Promise<Artist[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { skip, take, cursor, where, orderBy, include } = params;
|
||||||
return this.prisma.artist.findMany({
|
return this.prisma.artist.findMany({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
|
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
|
||||||
return this.prisma.artist.delete({
|
const ret = await this.prisma.artist.delete({
|
||||||
where,
|
where,
|
||||||
});
|
});
|
||||||
|
await this.search.index("artists").deleteDocument(ret.id);
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class CreateArtistDto {
|
export class CreateArtistDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
794
back/src/assetsgenerator/generateImages_browserless.js
Normal file
794
back/src/assetsgenerator/generateImages_browserless.js
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
// import Blob from "cross-blob";
|
||||||
|
import FS from "fs";
|
||||||
|
import jsdom from "jsdom";
|
||||||
|
//import headless_gl from "gl"; // this is now imported dynamically in a try catch, in case gl install fails, see #1160
|
||||||
|
import * as OSMD from "opensheetmusicdisplay"; // window needs to be available before we can require OSMD
|
||||||
|
|
||||||
|
let Blob;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Render each OSMD sample, grab the generated images, andg
|
||||||
|
dump them into a local directory as PNG or SVG files.
|
||||||
|
|
||||||
|
inspired by Vexflow's generate_png_images and vexflow-tests.js
|
||||||
|
|
||||||
|
This can be used to generate PNGs or SVGs from OSMD without a browser.
|
||||||
|
It's also used with the visual regression test system (using PNGs) in
|
||||||
|
`tools/visual_regression.sh`
|
||||||
|
(see package.json, used with npm run generate:blessed and generate:current, then test:visual).
|
||||||
|
|
||||||
|
Note: this script needs to "fake" quite a few browser elements, like window, document,
|
||||||
|
and a Canvas HTMLElement (for PNG) or the DOM (for SVG) ,
|
||||||
|
which otherwise are missing in pure nodejs, causing errors in OSMD.
|
||||||
|
For PNG it needs the canvas package installed.
|
||||||
|
There are also some hacks needed to set the container size (offsetWidth) correctly.
|
||||||
|
|
||||||
|
Otherwise you'd need to run a headless browser, which is way slower,
|
||||||
|
see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestampToMs = (timestamp, wholeNoteLength) => {
|
||||||
|
return timestamp.RealValue * wholeNoteLength;
|
||||||
|
};
|
||||||
|
const getActualNoteLength = (note, wholeNoteLength) => {
|
||||||
|
let duration = timestampToMs(note.Length, wholeNoteLength);
|
||||||
|
if (note.NoteTie) {
|
||||||
|
const firstNote = note.NoteTie.Notes.at(1);
|
||||||
|
if (Object.is(note.NoteTie.StartNote, note) && firstNote) {
|
||||||
|
duration += timestampToMs(firstNote.Length, wholeNoteLength);
|
||||||
|
} else {
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCursorPositions(osmd, filename, partitionDims) {
|
||||||
|
osmd.cursor.show();
|
||||||
|
const bpm = osmd.Sheet.HasBPMInfo
|
||||||
|
? osmd.Sheet.getExpressionsStartTempoInBPM()
|
||||||
|
: 60;
|
||||||
|
const wholeNoteLength = Math.round((60 / bpm) * 4000);
|
||||||
|
const curPos = [];
|
||||||
|
while (!osmd.cursor.iterator.EndReached) {
|
||||||
|
const notesToPlay = osmd.cursor
|
||||||
|
.NotesUnderCursor()
|
||||||
|
.filter((note) => {
|
||||||
|
return note.isRest() == false && note.Pitch;
|
||||||
|
})
|
||||||
|
.map((note) => {
|
||||||
|
const fixedKey =
|
||||||
|
note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)
|
||||||
|
?.fixedKey ?? 0;
|
||||||
|
const midiNumber = note.halfTone - fixedKey * 12;
|
||||||
|
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
|
||||||
|
return {
|
||||||
|
note: midiNumber,
|
||||||
|
gain: gain,
|
||||||
|
duration: getActualNoteLength(note, wholeNoteLength),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const shortestNotes = osmd.cursor
|
||||||
|
.NotesUnderCursor()
|
||||||
|
.sort((n1, n2) => n1.Length.CompareTo(n2.Length))
|
||||||
|
.at(0);
|
||||||
|
const ts = timestampToMs(
|
||||||
|
shortestNotes?.getAbsoluteTimestamp() ?? new OSMD.Fraction(-1),
|
||||||
|
wholeNoteLength,
|
||||||
|
);
|
||||||
|
const sNL = timestampToMs(
|
||||||
|
shortestNotes?.Length ?? new OSMD.Fraction(-1),
|
||||||
|
wholeNoteLength,
|
||||||
|
);
|
||||||
|
curPos.push({
|
||||||
|
x: parseFloat(osmd.cursor.cursorElement.style.left),
|
||||||
|
y: parseFloat(osmd.cursor.cursorElement.style.top),
|
||||||
|
width: osmd.cursor.cursorElement.width,
|
||||||
|
height: osmd.cursor.cursorElement.height,
|
||||||
|
notes: notesToPlay,
|
||||||
|
timestamp: ts,
|
||||||
|
timing: sNL,
|
||||||
|
});
|
||||||
|
osmd.cursor.next();
|
||||||
|
}
|
||||||
|
osmd.cursor.reset();
|
||||||
|
osmd.cursor.hide();
|
||||||
|
|
||||||
|
const cursorsFilename = `${imageDir}/${filename}.json`;
|
||||||
|
FS.writeFileSync(
|
||||||
|
cursorsFilename,
|
||||||
|
JSON.stringify({
|
||||||
|
pageWidth: partitionDims[0],
|
||||||
|
pageHeight: partitionDims[1],
|
||||||
|
cursors: curPos,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log(`Saved cursor positions to ${cursorsFilename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// global variables
|
||||||
|
// (without these being global, we'd have to pass many of these values to the generateSampleImage function)
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let assetName;
|
||||||
|
let sampleDir;
|
||||||
|
let imageDir;
|
||||||
|
let imageFormat;
|
||||||
|
let pageWidth;
|
||||||
|
let pageHeight;
|
||||||
|
let filterRegex;
|
||||||
|
let mode;
|
||||||
|
let debugSleepTimeString;
|
||||||
|
let skyBottomLinePreference;
|
||||||
|
let pageFormat;
|
||||||
|
|
||||||
|
export async function generateSongAssets(
|
||||||
|
assetName_,
|
||||||
|
sampleDir_,
|
||||||
|
imageDir_,
|
||||||
|
imageFormat_,
|
||||||
|
pageWidth_,
|
||||||
|
pageHeight_,
|
||||||
|
filterRegex_,
|
||||||
|
mode_,
|
||||||
|
debugSleepTimeString_,
|
||||||
|
skyBottomLinePreference_,
|
||||||
|
) {
|
||||||
|
assetName = assetName_;
|
||||||
|
sampleDir = sampleDir_;
|
||||||
|
imageDir = imageDir_;
|
||||||
|
imageFormat = imageFormat_;
|
||||||
|
pageWidth = pageWidth_;
|
||||||
|
pageHeight = pageHeight_;
|
||||||
|
filterRegex = filterRegex_;
|
||||||
|
mode = mode_;
|
||||||
|
debugSleepTimeString = debugSleepTimeString_;
|
||||||
|
skyBottomLinePreference = skyBottomLinePreference_;
|
||||||
|
imageFormat = imageFormat?.toLowerCase();
|
||||||
|
eval(`import("cross-blob")`).then((module) => {
|
||||||
|
Blob = module.default;
|
||||||
|
});
|
||||||
|
debug("" + sampleDir + " " + imageDir + " " + imageFormat);
|
||||||
|
|
||||||
|
if (!mode) {
|
||||||
|
mode = "";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!assetName ||
|
||||||
|
!sampleDir ||
|
||||||
|
!imageDir ||
|
||||||
|
(imageFormat !== "png" && imageFormat !== "svg")
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"usage: " +
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
"node test/Util/generateImages_browserless.mjs osmdBuildDir sampleDirectory imageDirectory svg|png [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
' (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"example: node test/Util/generateImages_browserless.mjs ../../build ./test/data/ ./export png",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
|
||||||
|
);
|
||||||
|
Promise.reject(
|
||||||
|
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// let OSMD; // can only be required once window was simulated
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
debug("init");
|
||||||
|
|
||||||
|
const osmdTestingMode = mode.includes("osmdtesting"); // can also be --debugosmdtesting
|
||||||
|
const osmdTestingSingleMode = mode.includes("osmdtestingsingle");
|
||||||
|
const DEBUG = mode.startsWith("--debug");
|
||||||
|
// const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
|
||||||
|
if (DEBUG) {
|
||||||
|
// debug(' (note that --debug slows down the script by about 0.3s per file, through logging)')
|
||||||
|
const debugSleepTimeMs = Number.parseInt(debugSleepTimeString, 10);
|
||||||
|
if (debugSleepTimeMs > 0) {
|
||||||
|
debug("debug sleep time: " + debugSleepTimeString);
|
||||||
|
await sleep(Number.parseInt(debugSleepTimeMs, 10));
|
||||||
|
// [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
|
||||||
|
// sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug("sampleDir: " + sampleDir, DEBUG);
|
||||||
|
debug("imageDir: " + imageDir, DEBUG);
|
||||||
|
debug("imageFormat: " + imageFormat, DEBUG);
|
||||||
|
|
||||||
|
pageFormat = "Endless";
|
||||||
|
pageWidth = Number.parseInt(pageWidth, 10);
|
||||||
|
pageHeight = Number.parseInt(pageHeight, 10);
|
||||||
|
const endlessPage = !(pageHeight > 0 && pageWidth > 0);
|
||||||
|
if (!endlessPage) {
|
||||||
|
pageFormat = `${pageWidth}x${pageHeight}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const dom = new jsdom.JSDOM("<!DOCTYPE html></html>");
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
// window = dom.window;
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
// document = dom.window.document;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
global.window = dom.window;
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
global.document = window.document;
|
||||||
|
//window.console = console; // probably does nothing
|
||||||
|
global.HTMLElement = window.HTMLElement;
|
||||||
|
global.HTMLAnchorElement = window.HTMLAnchorElement;
|
||||||
|
global.XMLHttpRequest = window.XMLHttpRequest;
|
||||||
|
global.DOMParser = window.DOMParser;
|
||||||
|
global.Node = window.Node;
|
||||||
|
if (imageFormat === "png") {
|
||||||
|
global.Canvas = window.Canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For WebGLSkyBottomLineCalculatorBackend: Try to import gl dynamically
|
||||||
|
// this is so that the script doesn't fail if gl could not be installed,
|
||||||
|
// which can happen in some linux setups where gcc-11 is installed, see #1160
|
||||||
|
try {
|
||||||
|
const { default: headless_gl } = await import("gl");
|
||||||
|
const oldCreateElement = document.createElement.bind(document);
|
||||||
|
document.createElement = function (tagName, options) {
|
||||||
|
if (tagName.toLowerCase() === "canvas") {
|
||||||
|
const canvas = oldCreateElement(tagName, options);
|
||||||
|
const oldGetContext = canvas.getContext.bind(canvas);
|
||||||
|
canvas.getContext = function (contextType, contextAttributes) {
|
||||||
|
if (
|
||||||
|
contextType.toLowerCase() === "webgl" ||
|
||||||
|
contextType.toLowerCase() === "experimental-webgl"
|
||||||
|
) {
|
||||||
|
const gl = headless_gl(
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
contextAttributes,
|
||||||
|
);
|
||||||
|
gl.canvas = canvas;
|
||||||
|
return gl;
|
||||||
|
} else {
|
||||||
|
return oldGetContext(contextType, contextAttributes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return canvas;
|
||||||
|
} else {
|
||||||
|
return oldCreateElement(tagName, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
if (skyBottomLinePreference === "--webgl") {
|
||||||
|
debug(
|
||||||
|
"WebGL image generation was requested but gl is not installed; using non-WebGL generation.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix Blob not found (to support external modules like is-blob)
|
||||||
|
global.Blob = Blob;
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.id = "browserlessDiv";
|
||||||
|
document.body.appendChild(div);
|
||||||
|
// const canvas = document.createElement('canvas')
|
||||||
|
// div.canvas = document.createElement('canvas')
|
||||||
|
|
||||||
|
const zoom = 1.0;
|
||||||
|
// width of the div / PNG generated
|
||||||
|
let width = pageWidth * zoom;
|
||||||
|
// TODO sometimes the width is way too small for the score, may need to adjust zoom.
|
||||||
|
if (endlessPage) {
|
||||||
|
width = 1440;
|
||||||
|
}
|
||||||
|
let height = pageHeight;
|
||||||
|
if (endlessPage) {
|
||||||
|
height = 32767;
|
||||||
|
}
|
||||||
|
div.width = width;
|
||||||
|
div.height = height;
|
||||||
|
// div.offsetWidth = width; // doesn't work, offsetWidth is always 0 from this. see below
|
||||||
|
// div.clientWidth = width;
|
||||||
|
// div.clientHeight = height;
|
||||||
|
// div.scrollHeight = height;
|
||||||
|
// div.scrollWidth = width;
|
||||||
|
div.setAttribute("width", width);
|
||||||
|
div.setAttribute("height", height);
|
||||||
|
div.setAttribute("offsetWidth", width);
|
||||||
|
// debug('div.offsetWidth: ' + div.offsetWidth, DEBUG) // 0 here, set correctly later
|
||||||
|
// debug('div.height: ' + div.height, DEBUG)
|
||||||
|
|
||||||
|
// hack: set offsetWidth reliably
|
||||||
|
Object.defineProperties(window.HTMLElement.prototype, {
|
||||||
|
offsetLeft: {
|
||||||
|
get: function () {
|
||||||
|
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offsetTop: {
|
||||||
|
get: function () {
|
||||||
|
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offsetHeight: {
|
||||||
|
get: function () {
|
||||||
|
return height;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offsetWidth: {
|
||||||
|
get: function () {
|
||||||
|
return width;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
debug("div.offsetWidth: " + div.offsetWidth, DEBUG);
|
||||||
|
debug("div.height: " + div.height, DEBUG);
|
||||||
|
// ---- end browser hacks (hopefully) ----
|
||||||
|
|
||||||
|
// load globally
|
||||||
|
|
||||||
|
// Create the image directory if it doesn't exist.
|
||||||
|
FS.mkdirSync(imageDir, { recursive: true });
|
||||||
|
|
||||||
|
// const sampleDirFilenames = FS.readdirSync(sampleDir);
|
||||||
|
let samplesToProcess = []; // samples we want to process/generate pngs of, excluding the filtered out files/filenames
|
||||||
|
|
||||||
|
// sampleDir is the direct path to a single file but is then only keept as a the directory containing the file
|
||||||
|
if (sampleDir.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
|
||||||
|
let pathParts = sampleDir.split("/");
|
||||||
|
let filename = pathParts[pathParts.length - 1];
|
||||||
|
sampleDir = pathParts.slice(0, pathParts.length - 1).join("/");
|
||||||
|
samplesToProcess.push(filename);
|
||||||
|
} else {
|
||||||
|
debug("not a correct extension sampleDir: " + sampleDir, DEBUG);
|
||||||
|
}
|
||||||
|
// for (const sampleFilename of sampleDirFilenames) {
|
||||||
|
// if (osmdTestingMode && filterRegex === "allSmall") {
|
||||||
|
// if (sampleFilename.match("^(Actor)|(Gounod)")) {
|
||||||
|
// // TODO maybe filter by file size instead
|
||||||
|
// debug("filtering big file: " + sampleFilename, DEBUG);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // eslint-disable-next-line no-useless-escape
|
||||||
|
// if (sampleFilename.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
|
||||||
|
// // debug('found musicxml/mxl: ' + sampleFilename)
|
||||||
|
// samplesToProcess.push(sampleFilename);
|
||||||
|
// } else {
|
||||||
|
// debug("discarded file/directory: " + sampleFilename, DEBUG);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// filter samples to process by regex if given
|
||||||
|
if (
|
||||||
|
filterRegex &&
|
||||||
|
filterRegex !== "" &&
|
||||||
|
filterRegex !== "all" &&
|
||||||
|
!(osmdTestingMode && filterRegex === "allSmall")
|
||||||
|
) {
|
||||||
|
debug("filtering samples for regex: " + filterRegex, DEBUG);
|
||||||
|
samplesToProcess = samplesToProcess.filter((filename) =>
|
||||||
|
filename.match(filterRegex),
|
||||||
|
);
|
||||||
|
debug(`found ${samplesToProcess.length} matches: `, DEBUG);
|
||||||
|
for (let i = 0; i < samplesToProcess.length; i++) {
|
||||||
|
debug(samplesToProcess[i], DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backend = imageFormat === "png" ? "canvas" : "svg";
|
||||||
|
const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
|
||||||
|
autoResize: false,
|
||||||
|
backend: backend,
|
||||||
|
pageBackgroundColor: "#FFFFFF",
|
||||||
|
pageFormat: pageFormat,
|
||||||
|
// defaultFontFamily: 'Arial',
|
||||||
|
drawTitle: false,
|
||||||
|
renderSingleHorizontalStaffline: true,
|
||||||
|
drawComposer: false,
|
||||||
|
drawCredits: false,
|
||||||
|
drawLyrics: false,
|
||||||
|
drawPartNames: false,
|
||||||
|
followCursor: false,
|
||||||
|
cursorsOptions: [{ type: 0, color: "green", alpha: 0.5, follow: false }],
|
||||||
|
});
|
||||||
|
// for more options check OSMDOptions.ts
|
||||||
|
|
||||||
|
// you can set finer-grained rendering/engraving settings in EngravingRules:
|
||||||
|
// osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 5.0 is default
|
||||||
|
// (unless in osmdTestingMode, these will be reset with drawingParameters default)
|
||||||
|
// osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
|
||||||
|
// osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
|
||||||
|
// note that for now the png and canvas will still have the height given in the script argument,
|
||||||
|
// so even with a margin of 0 the image will be filled to the full height.
|
||||||
|
// osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
|
||||||
|
// osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
|
||||||
|
// osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
|
||||||
|
// osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
|
||||||
|
// for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
osmdInstance.setLogLevel("debug");
|
||||||
|
// debug(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
|
||||||
|
debug(
|
||||||
|
`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`,
|
||||||
|
);
|
||||||
|
debug("PageHeight: " + osmdInstance.EngravingRules.PageHeight);
|
||||||
|
} else {
|
||||||
|
osmdInstance.setLogLevel("info"); // doesn't seem to work, log.debug still logs
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(
|
||||||
|
"[OSMD.generateImages] starting loop over samples, saving images to " +
|
||||||
|
imageDir,
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
for (let i = 0; i < samplesToProcess.length; i++) {
|
||||||
|
const sampleFilename = samplesToProcess[i];
|
||||||
|
debug("sampleFilename: " + sampleFilename, DEBUG);
|
||||||
|
|
||||||
|
await generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
sampleDir,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
{},
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
osmdTestingMode &&
|
||||||
|
!osmdTestingSingleMode &&
|
||||||
|
sampleFilename.startsWith("Beethoven") &&
|
||||||
|
sampleFilename.includes("Geliebte")
|
||||||
|
) {
|
||||||
|
// generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
|
||||||
|
await generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
sampleDir,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
{ skyBottomLine: true },
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
// generate one more testing image with GraphicalNote positions
|
||||||
|
await generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
sampleDir,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
{ boundingBoxes: "VexFlowGraphicalNote" },
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("done, exiting.");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
|
||||||
|
async function generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
directory,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
options = {},
|
||||||
|
DEBUG = false,
|
||||||
|
) {
|
||||||
|
function makeSkyBottomLineOptions() {
|
||||||
|
const preference = skyBottomLinePreference ?? "";
|
||||||
|
if (preference === "--batch") {
|
||||||
|
return {
|
||||||
|
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
|
||||||
|
skyBottomLineBatchCriteria: 0, // use batch algorithm only
|
||||||
|
};
|
||||||
|
} else if (preference === "--webgl") {
|
||||||
|
return {
|
||||||
|
preferredSkyBottomLineBatchCalculatorBackend: 1, // webgl
|
||||||
|
skyBottomLineBatchCriteria: 0, // use batch algorithm only
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
|
||||||
|
skyBottomLineBatchCriteria: Infinity, // use non-batch algorithm only
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const samplePath = directory + "/" + sampleFilename;
|
||||||
|
let loadParameter = FS.readFileSync(samplePath);
|
||||||
|
|
||||||
|
if (sampleFilename.endsWith(".mxl")) {
|
||||||
|
loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter);
|
||||||
|
} else {
|
||||||
|
loadParameter = loadParameter.toString();
|
||||||
|
}
|
||||||
|
// debug('loadParameter: ' + loadParameter)
|
||||||
|
// debug('typeof loadParameter: ' + typeof loadParameter)
|
||||||
|
|
||||||
|
// set sample-specific options for OSMD visual regression testing
|
||||||
|
let includeSkyBottomLine = false;
|
||||||
|
let drawBoundingBoxString;
|
||||||
|
let isTestOctaveShiftInvisibleInstrument;
|
||||||
|
let isTestInvisibleMeasureNotAffectingLayout;
|
||||||
|
if (osmdTestingMode) {
|
||||||
|
const isFunctionTestAutobeam = sampleFilename.startsWith(
|
||||||
|
"OSMD_function_test_autobeam",
|
||||||
|
);
|
||||||
|
const isFunctionTestAutoColoring = sampleFilename.startsWith(
|
||||||
|
"OSMD_function_test_auto-custom-coloring",
|
||||||
|
);
|
||||||
|
const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith(
|
||||||
|
"OSMD_Function_Test_System_and_Page_Breaks",
|
||||||
|
);
|
||||||
|
const isFunctionTestDrawingRange = sampleFilename.startsWith(
|
||||||
|
"OSMD_function_test_measuresToDraw_",
|
||||||
|
);
|
||||||
|
const defaultOrCompactTightMode = sampleFilename.startsWith(
|
||||||
|
"OSMD_Function_Test_Container_height",
|
||||||
|
)
|
||||||
|
? "compacttight"
|
||||||
|
: "default";
|
||||||
|
const isTestFlatBeams = sampleFilename.startsWith("test_drum_tuplet_beams");
|
||||||
|
const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith(
|
||||||
|
"test_end_measure_clefs_staffentry_bbox",
|
||||||
|
);
|
||||||
|
const isTestPageBreakImpliesSystemBreak = sampleFilename.startsWith(
|
||||||
|
"test_pagebreak_implies_systembreak",
|
||||||
|
);
|
||||||
|
const isTestPageBottomMargin0 =
|
||||||
|
sampleFilename.includes("PageBottomMargin0");
|
||||||
|
const isTestTupletBracketTupletNumber = sampleFilename.includes(
|
||||||
|
"test_tuplet_bracket_tuplet_number",
|
||||||
|
);
|
||||||
|
const isTestCajon2NoteSystem = sampleFilename.includes(
|
||||||
|
"test_cajon_2-note-system",
|
||||||
|
);
|
||||||
|
isTestOctaveShiftInvisibleInstrument = sampleFilename.includes(
|
||||||
|
"test_octaveshift_first_instrument_invisible",
|
||||||
|
);
|
||||||
|
const isTextOctaveShiftExtraGraphicalMeasure = sampleFilename.includes(
|
||||||
|
"test_octaveshift_extragraphicalmeasure",
|
||||||
|
);
|
||||||
|
isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes(
|
||||||
|
"test_invisible_measure_not_affecting_layout",
|
||||||
|
);
|
||||||
|
const isTestWedgeMultilineCrescendo = sampleFilename.includes(
|
||||||
|
"test_wedge_multiline_crescendo",
|
||||||
|
);
|
||||||
|
const isTestWedgeMultilineDecrescendo = sampleFilename.includes(
|
||||||
|
"test_wedge_multiline_decrescendo",
|
||||||
|
);
|
||||||
|
osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
|
||||||
|
if (isTestEndClefStaffEntryBboxes) {
|
||||||
|
drawBoundingBoxString = "VexFlowStaffEntry";
|
||||||
|
} else {
|
||||||
|
drawBoundingBoxString = options.boundingBoxes; // undefined is also a valid value: no bboxes
|
||||||
|
}
|
||||||
|
osmdInstance.setOptions({
|
||||||
|
autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
|
||||||
|
coloringMode: isFunctionTestAutoColoring ? 2 : 0,
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
coloringSetCustom: isFunctionTestAutoColoring
|
||||||
|
? [
|
||||||
|
"#d82c6b",
|
||||||
|
"#F89D15",
|
||||||
|
"#FFE21A",
|
||||||
|
"#4dbd5c",
|
||||||
|
"#009D96",
|
||||||
|
"#43469d",
|
||||||
|
"#76429c",
|
||||||
|
"#ff0000",
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
|
colorStemsLikeNoteheads: isFunctionTestAutoColoring,
|
||||||
|
drawingParameters: defaultOrCompactTightMode, // note: default resets all EngravingRules. could be solved differently
|
||||||
|
drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
|
||||||
|
drawUpToMeasureNumber: isFunctionTestDrawingRange
|
||||||
|
? 12
|
||||||
|
: Number.MAX_SAFE_INTEGER,
|
||||||
|
newSystemFromXML: isFunctionTestSystemAndPageBreaks,
|
||||||
|
newSystemFromNewPageInXML: isTestPageBreakImpliesSystemBreak,
|
||||||
|
newPageFromXML: isFunctionTestSystemAndPageBreaks,
|
||||||
|
pageBackgroundColor: "#FFFFFF", // reset by drawingparameters default
|
||||||
|
pageFormat: pageFormat, // reset by drawingparameters default,
|
||||||
|
...makeSkyBottomLineOptions(),
|
||||||
|
});
|
||||||
|
// note that loadDefaultValues() may be executed in setOptions with drawingParameters default
|
||||||
|
//osmdInstance.EngravingRules.RenderSingleHorizontalStaffline = true; // to use this option here, place it after setOptions(), see above
|
||||||
|
osmdInstance.EngravingRules.AlwaysSetPreferredSkyBottomLineBackendAutomatically = false; // this would override the command line options (--plain etc)
|
||||||
|
includeSkyBottomLine = options.skyBottomLine
|
||||||
|
? options.skyBottomLine
|
||||||
|
: false; // apparently es6 doesn't have ?? operator
|
||||||
|
osmdInstance.drawSkyLine = includeSkyBottomLine; // if includeSkyBottomLine, draw skyline and bottomline, else not
|
||||||
|
osmdInstance.drawBottomLine = includeSkyBottomLine;
|
||||||
|
osmdInstance.setDrawBoundingBox(drawBoundingBoxString, false); // false: don't render (now). also (re-)set if undefined!
|
||||||
|
if (isTestFlatBeams) {
|
||||||
|
osmdInstance.EngravingRules.FlatBeams = true;
|
||||||
|
// osmdInstance.EngravingRules.FlatBeamOffset = 30;
|
||||||
|
osmdInstance.EngravingRules.FlatBeamOffset = 10;
|
||||||
|
osmdInstance.EngravingRules.FlatBeamOffsetPerBeam = 10;
|
||||||
|
} else {
|
||||||
|
osmdInstance.EngravingRules.FlatBeams = false;
|
||||||
|
}
|
||||||
|
if (isTestPageBottomMargin0) {
|
||||||
|
osmdInstance.EngravingRules.PageBottomMargin = 0;
|
||||||
|
}
|
||||||
|
if (isTestTupletBracketTupletNumber) {
|
||||||
|
osmdInstance.EngravingRules.TupletNumberLimitConsecutiveRepetitions = true;
|
||||||
|
osmdInstance.EngravingRules.TupletNumberMaxConsecutiveRepetitions = 2;
|
||||||
|
osmdInstance.EngravingRules.TupletNumberAlwaysDisableAfterFirstMax = true; // necessary to trigger bug
|
||||||
|
}
|
||||||
|
if (isTestCajon2NoteSystem) {
|
||||||
|
osmdInstance.EngravingRules.PercussionUseCajon2NoteSystem = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isTextOctaveShiftExtraGraphicalMeasure ||
|
||||||
|
isTestOctaveShiftInvisibleInstrument ||
|
||||||
|
isTestWedgeMultilineCrescendo ||
|
||||||
|
isTestWedgeMultilineDecrescendo
|
||||||
|
) {
|
||||||
|
osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug("loading sample " + sampleFilename, DEBUG);
|
||||||
|
await osmdInstance.load(loadParameter, sampleFilename); // if using load.then() without await, memory will not be freed up between renders
|
||||||
|
if (isTestOctaveShiftInvisibleInstrument) {
|
||||||
|
osmdInstance.Sheet.Instruments[0].Visible = false;
|
||||||
|
}
|
||||||
|
if (isTestInvisibleMeasureNotAffectingLayout) {
|
||||||
|
if (osmdInstance.Sheet.Instruments[1]) {
|
||||||
|
// some systems can't handle ?. in this script (just a safety check anyways)
|
||||||
|
osmdInstance.Sheet.Instruments[1].Visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
debug(
|
||||||
|
"couldn't load sample " + sampleFilename + ", skipping. Error: \n" + ex,
|
||||||
|
);
|
||||||
|
return Promise.reject(ex);
|
||||||
|
}
|
||||||
|
debug("xml loaded", DEBUG);
|
||||||
|
try {
|
||||||
|
osmdInstance.render();
|
||||||
|
// there were reports that await could help here, but render isn't a synchronous function, and it seems to work. see #932
|
||||||
|
} catch (ex) {
|
||||||
|
debug("renderError: " + ex);
|
||||||
|
}
|
||||||
|
debug("rendered", DEBUG);
|
||||||
|
|
||||||
|
const markupStrings = []; // svg
|
||||||
|
const dataUrls = []; // png
|
||||||
|
let canvasImage;
|
||||||
|
|
||||||
|
// intended to use only for the chromacase partition use case (always 1 page in svg)
|
||||||
|
let partitionDims = [-1, -1];
|
||||||
|
|
||||||
|
for (
|
||||||
|
let pageNumber = 1;
|
||||||
|
pageNumber < Number.POSITIVE_INFINITY;
|
||||||
|
pageNumber++
|
||||||
|
) {
|
||||||
|
if (imageFormat === "png") {
|
||||||
|
canvasImage = document.getElementById(
|
||||||
|
"osmdCanvasVexFlowBackendCanvas" + pageNumber,
|
||||||
|
);
|
||||||
|
if (!canvasImage) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!canvasImage.toDataURL) {
|
||||||
|
debug(
|
||||||
|
`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dataUrls.push(canvasImage.toDataURL());
|
||||||
|
} else if (imageFormat === "svg") {
|
||||||
|
const svgElement = document.getElementById("osmdSvgPage" + pageNumber);
|
||||||
|
if (!svgElement) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// The important xmlns attribute is not serialized unless we set it here
|
||||||
|
svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||||
|
const width = svgElement.getAttribute("width");
|
||||||
|
const height = svgElement.getAttribute("height");
|
||||||
|
partitionDims = [width, height];
|
||||||
|
markupStrings.push(svgElement.outerHTML);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the cursor positions file
|
||||||
|
getCursorPositions(osmdInstance, assetName, partitionDims);
|
||||||
|
|
||||||
|
for (
|
||||||
|
let pageIndex = 0;
|
||||||
|
pageIndex < Math.max(dataUrls.length, markupStrings.length);
|
||||||
|
pageIndex++
|
||||||
|
) {
|
||||||
|
const pageNumberingString = `${pageIndex + 1}`;
|
||||||
|
const skybottomlineString = includeSkyBottomLine ? "skybottomline_" : "";
|
||||||
|
const graphicalNoteBboxesString = drawBoundingBoxString
|
||||||
|
? "bbox" + drawBoundingBoxString + "_"
|
||||||
|
: "";
|
||||||
|
// pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
|
||||||
|
const pageFilename = `${imageDir}/${assetName}.${imageFormat}`;
|
||||||
|
|
||||||
|
if (imageFormat === "png") {
|
||||||
|
const dataUrl = dataUrls[pageIndex];
|
||||||
|
if (!dataUrl || !dataUrl.split) {
|
||||||
|
debug(
|
||||||
|
`error: could not get dataUrl (imageData) for page ${
|
||||||
|
pageIndex + 1
|
||||||
|
} of sample: ${sampleFilename}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const imageData = dataUrl.split(";base64,").pop();
|
||||||
|
const imageBuffer = Buffer.from(imageData, "base64");
|
||||||
|
|
||||||
|
debug("got image data, saving to: " + pageFilename, DEBUG);
|
||||||
|
FS.writeFileSync(pageFilename, imageBuffer, { encoding: "base64" });
|
||||||
|
} else if (imageFormat === "svg") {
|
||||||
|
const markup = markupStrings[pageIndex];
|
||||||
|
if (!markup) {
|
||||||
|
debug(
|
||||||
|
`error: could not get markup (SVG data) for page ${
|
||||||
|
pageIndex + 1
|
||||||
|
} of sample: ${sampleFilename}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("got svg markup data, saving to: " + pageFilename, DEBUG);
|
||||||
|
// replace every bounding-box by none (react native doesn't support bounding-box)
|
||||||
|
FS.writeFileSync(pageFilename, markup.replace(/bounding-box/g, "none"), {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug: log memory usage
|
||||||
|
// const usage = process.memoryUsage()
|
||||||
|
// for (const entry of Object.entries(usage)) {
|
||||||
|
// if (entry[0] === 'rss') {
|
||||||
|
// if (entry[1] > maxRss) {
|
||||||
|
// maxRss = entry[1]
|
||||||
|
// maxRssFilename = pageFilename
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// debug(entry[0] + ': ' + entry[1] / (1024 * 1024) + 'mb')
|
||||||
|
// }
|
||||||
|
// debug('maxRss: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
|
||||||
|
}
|
||||||
|
// debug('maxRss total: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
|
||||||
|
|
||||||
|
// await sleep(5000)
|
||||||
|
// }) // end read file
|
||||||
|
}
|
||||||
|
|
||||||
|
function debug(msg, debugEnabled = true) {
|
||||||
|
if (debugEnabled) {
|
||||||
|
console.log("[generateImages] " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init();
|
||||||
5
back/src/auth/apikey-auth.guard.ts
Normal file
5
back/src/auth/apikey-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyAuthGuard extends AuthGuard("api-key") {}
|
||||||
31
back/src/auth/apikey.strategy.ts
Normal file
31
back/src/auth/apikey.strategy.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
|
import Strategy from "passport-headerapikey";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HeaderApiKeyStrategy extends PassportStrategy(
|
||||||
|
Strategy,
|
||||||
|
"api-key",
|
||||||
|
) {
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
super(
|
||||||
|
{ header: "Authorization", prefix: "API Key " },
|
||||||
|
true,
|
||||||
|
async (apiKey, done) => {
|
||||||
|
return this.validate(apiKey, done);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public validate = (apiKey: string, done: (error: Error, data) => {}) => {
|
||||||
|
if (
|
||||||
|
this.configService.get<string>("API_KEYS")?.split(",").includes(apiKey)
|
||||||
|
) {
|
||||||
|
//@ts-expect-error
|
||||||
|
done(null, true);
|
||||||
|
}
|
||||||
|
done(new UnauthorizedException(), null);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,34 +7,54 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Delete,
|
Delete,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
Put,
|
Put,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
Patch,
|
Patch,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
Req,
|
||||||
import { AuthService } from './auth.service';
|
UseInterceptors,
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
UploadedFile,
|
||||||
import { LocalAuthGuard } from './local-auth.guard';
|
HttpStatus,
|
||||||
import { RegisterDto } from './dto/register.dto';
|
ParseFilePipeBuilder,
|
||||||
import { UsersService } from 'src/users/users.service';
|
Response,
|
||||||
|
Query,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
import { JwtAuthGuard } from "./jwt-auth.guard";
|
||||||
|
import { LocalAuthGuard } from "./local-auth.guard";
|
||||||
|
import { RegisterDto } from "./dto/register.dto";
|
||||||
|
import { UsersService } from "src/users/users.service";
|
||||||
import {
|
import {
|
||||||
|
ApiBadRequestResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiBody,
|
ApiBody,
|
||||||
|
ApiConflictResponse,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { User } from '../models/user';
|
import { User } from "../models/user";
|
||||||
import { JwtToken } from './models/jwt';
|
import { JwtToken } from "./models/jwt";
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from "./dto/login.dto";
|
||||||
import { Profile } from './dto/profile.dto';
|
import { Profile } from "./dto/profile.dto";
|
||||||
import { Setting } from 'src/models/setting';
|
import { Setting } from "src/models/setting";
|
||||||
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
|
import { UpdateSettingDto } from "src/settings/dto/update-setting.dto";
|
||||||
import { SettingsService } from 'src/settings/settings.service';
|
import { SettingsService } from "src/settings/settings.service";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import { writeFile } from "fs";
|
||||||
|
import { PasswordResetDto } from "./dto/password_reset.dto ";
|
||||||
|
import { mapInclude } from "src/utils/include";
|
||||||
|
import { SongController } from "src/song/song.controller";
|
||||||
|
import { ChromaAuthGuard } from "./chroma-auth.guard";
|
||||||
|
|
||||||
@ApiTags('auth')
|
@ApiTags("auth")
|
||||||
@Controller('auth')
|
@Controller("auth")
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
@@ -42,49 +62,167 @@ export class AuthController {
|
|||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('register')
|
@Get("login/google")
|
||||||
|
@UseGuards(AuthGuard("google"))
|
||||||
|
@ApiOperation({ description: "Redirect to google login page" })
|
||||||
|
googleLogin() {}
|
||||||
|
|
||||||
|
@Get("logged/google")
|
||||||
|
@ApiOperation({
|
||||||
|
description:
|
||||||
|
"Redirect to the front page after connecting to the google account",
|
||||||
|
})
|
||||||
|
@UseGuards(AuthGuard("google"))
|
||||||
|
async googleLoginCallbakc(@Req() req: any) {
|
||||||
|
let user = await this.usersService.user({ googleID: req.user.googleID });
|
||||||
|
if (!user) {
|
||||||
|
user = await this.usersService.createUser(req.user);
|
||||||
|
await this.settingsService.createUserSetting(user.id);
|
||||||
|
}
|
||||||
|
return this.authService.login(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("register")
|
||||||
|
@ApiOperation({ description: "Register a new user" })
|
||||||
|
@ApiConflictResponse({ description: "Username or email already taken" })
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Successfully registered, email sent to verify",
|
||||||
|
})
|
||||||
|
@ApiBadRequestResponse({ description: "Invalid data or database error" })
|
||||||
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const user = await this.usersService.createUser(registerDto)
|
const user = await this.usersService.createUser(registerDto);
|
||||||
await this.settingsService.createUserSetting(user.id);
|
await this.settingsService.createUserSetting(user.id);
|
||||||
} catch(e) {
|
await this.authService.sendVerifyMail(user);
|
||||||
|
} catch (e) {
|
||||||
|
// check if the error is a duplicate key error
|
||||||
|
if (e.code === "P2002") {
|
||||||
|
throw new ConflictException("Username or email already taken");
|
||||||
|
}
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put("verify")
|
||||||
|
@HttpCode(200)
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiOperation({ description: "Verify the email of the user" })
|
||||||
|
@ApiOkResponse({ description: "Successfully verified" })
|
||||||
|
@ApiBadRequestResponse({ description: "Invalid or expired token" })
|
||||||
|
async verify(
|
||||||
|
@Request() req: any,
|
||||||
|
@Query("token") token: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (await this.authService.verifyMail(req.user.id, token)) return;
|
||||||
|
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("reverify")
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@HttpCode(200)
|
||||||
|
@ApiOperation({ description: "Resend the verification email" })
|
||||||
|
async reverify(@Request() req: any): Promise<void> {
|
||||||
|
const user = await this.usersService.user({ id: req.user.id });
|
||||||
|
if (!user) throw new BadRequestException("Invalid user");
|
||||||
|
await this.authService.sendVerifyMail(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(200)
|
||||||
|
@Put("password-reset")
|
||||||
|
async password_reset(
|
||||||
|
@Body() resetDto: PasswordResetDto,
|
||||||
|
@Query("token") token: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (await this.authService.changePassword(resetDto.password, token)) return;
|
||||||
|
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(200)
|
||||||
|
@Put("forgot-password")
|
||||||
|
async forgot_password(@Query("email") email: string): Promise<void> {
|
||||||
|
console.log(email);
|
||||||
|
const user = await this.usersService.user({ email: email });
|
||||||
|
if (!user) throw new BadRequestException("Invalid user");
|
||||||
|
await this.authService.sendPasswordResetMail(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("login")
|
||||||
@ApiBody({ type: LoginDto })
|
@ApiBody({ type: LoginDto })
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(LocalAuthGuard)
|
@UseGuards(LocalAuthGuard)
|
||||||
@Post('login')
|
@ApiBody({ type: LoginDto })
|
||||||
|
@ApiOperation({ description: "Login with username and password" })
|
||||||
|
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid credentials" })
|
||||||
async login(@Request() req: any): Promise<JwtToken> {
|
async login(@Request() req: any): Promise<JwtToken> {
|
||||||
return this.authService.login(req.user);
|
return this.authService.login(req.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("guest")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@Post('guest')
|
@ApiOperation({ description: "Login as a guest account" })
|
||||||
|
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||||
async guest(): Promise<JwtToken> {
|
async guest(): Promise<JwtToken> {
|
||||||
const user = await this.usersService.createGuest();
|
const user = await this.usersService.createGuest();
|
||||||
await this.settingsService.createUserSetting(user.id);
|
await this.settingsService.createUserSetting(user.id);
|
||||||
return this.authService.login(user);
|
return this.authService.login(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(ChromaAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ description: "Get the profile picture of connected user" })
|
||||||
|
@ApiOkResponse({ description: "The user profile picture" })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
|
@Get("me/picture")
|
||||||
|
async getProfilePicture(@Request() req: any, @Response() res: any) {
|
||||||
|
return await this.usersService.getProfilePicture(req.user.id, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(ChromaAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOkResponse({ description: "The user profile picture" })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
|
@Post("me/picture")
|
||||||
|
@ApiOperation({ description: "Upload a new profile picture" })
|
||||||
|
@UseInterceptors(FileInterceptor("file"))
|
||||||
|
async postProfilePicture(
|
||||||
|
@Request() req: any,
|
||||||
|
@UploadedFile(
|
||||||
|
new ParseFilePipeBuilder()
|
||||||
|
.addFileTypeValidator({
|
||||||
|
fileType: "jpeg",
|
||||||
|
})
|
||||||
|
.build({
|
||||||
|
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
file: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
const path = `/data/${req.user.id}.jpg`;
|
||||||
|
writeFile(path, file.buffer, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully logged in', type: User })
|
@ApiOkResponse({ description: "Successfully logged in", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me')
|
@Get("me")
|
||||||
|
@ApiOperation({ description: "Get the user info of connected user" })
|
||||||
async getProfile(@Request() req: any): Promise<User> {
|
async getProfile(@Request() req: any): Promise<User> {
|
||||||
const user = await this.usersService.user({ id: req.user.id });
|
const user = await this.usersService.user({ id: req.user.id });
|
||||||
if (!user) throw new InternalServerErrorException();
|
if (!user) throw new InternalServerErrorException();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully edited profile', type: User })
|
@ApiOkResponse({ description: "Successfully edited profile", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Put('me')
|
@Put("me")
|
||||||
|
@ApiOperation({ description: "Edit the profile of connected user" })
|
||||||
editProfile(
|
editProfile(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() profile: Partial<Profile>,
|
@Body() profile: Partial<Profile>,
|
||||||
@@ -107,35 +245,83 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully deleted', type: User })
|
@ApiOkResponse({ description: "Successfully deleted", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Delete('me')
|
@Delete("me")
|
||||||
|
@ApiOperation({ description: "Delete the profile of connected user" })
|
||||||
deleteSelf(@Request() req: any): Promise<User> {
|
deleteSelf(@Request() req: any): Promise<User> {
|
||||||
return this.usersService.deleteUser({ id: req.user.id });
|
return this.usersService.deleteUser({ id: req.user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
|
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||||
@ApiUnauthorizedResponse({description: 'Invalid token'})
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Patch('me/settings')
|
@Patch("me/settings")
|
||||||
|
@ApiOperation({ description: "Edit the settings of connected user" })
|
||||||
udpateSettings(
|
udpateSettings(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() settingUserDto: UpdateSettingDto): Promise<Setting> {
|
@Body() settingUserDto: UpdateSettingDto,
|
||||||
|
): Promise<Setting> {
|
||||||
return this.settingsService.updateUserSettings({
|
return this.settingsService.updateUserSettings({
|
||||||
where: { userId: +req.user.id},
|
where: { userId: +req.user.id },
|
||||||
data: settingUserDto,
|
data: settingUserDto,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
|
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||||
@ApiUnauthorizedResponse({description: 'Invalid token'})
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me/settings')
|
@Get("me/settings")
|
||||||
|
@ApiOperation({ description: "Get the settings of connected user" })
|
||||||
async getSettings(@Request() req: any): Promise<Setting> {
|
async getSettings(@Request() req: any): Promise<Setting> {
|
||||||
const result = await this.settingsService.getUserSetting({ userId: +req.user.id });
|
const result = await this.settingsService.getUserSetting({
|
||||||
|
userId: +req.user.id,
|
||||||
|
});
|
||||||
if (!result) throw new NotFoundException();
|
if (!result) throw new NotFoundException();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOkResponse({ description: "Successfully added liked song" })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
|
@Post("me/likes/:id")
|
||||||
|
addLikedSong(@Request() req: any, @Param("id", ParseIntPipe) songId: number) {
|
||||||
|
return this.usersService.addLikedSong(+req.user.id, songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOkResponse({ description: "Successfully removed liked song" })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
|
@Delete("me/likes/:id")
|
||||||
|
removeLikedSong(
|
||||||
|
@Request() req: any,
|
||||||
|
@Param("id", ParseIntPipe) songId: number,
|
||||||
|
) {
|
||||||
|
return this.usersService.removeLikedSong(+req.user.id, songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOkResponse({ description: "Successfully retrieved liked song" })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
|
@Get("me/likes")
|
||||||
|
getLikedSongs(@Request() req: any, @Query("include") include: string) {
|
||||||
|
return this.usersService.getLikedSongs(
|
||||||
|
+req.user.id,
|
||||||
|
mapInclude(include, req, SongController.includableFields),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOkResponse({ description: "Successfully added score" })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
|
@Patch("me/score/:score")
|
||||||
|
addScore(@Request() req: any, @Param("score", ParseIntPipe) score: number) {
|
||||||
|
return this.usersService.addScore(+req.user.id, score);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { UsersModule } from 'src/users/users.module';
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from "./auth.service";
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from "@nestjs/passport";
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from "./auth.controller";
|
||||||
import { LocalStrategy } from './local.strategy';
|
import { LocalStrategy } from "./local.strategy";
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from "@nestjs/config";
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { JwtStrategy } from './jwt.strategy';
|
import { JwtStrategy } from "./jwt.strategy";
|
||||||
import { SettingsModule } from 'src/settings/settings.module';
|
import { SettingsModule } from "src/settings/settings.module";
|
||||||
|
import { GoogleStrategy } from "./google.strategy";
|
||||||
|
import { HeaderApiKeyStrategy } from "./apikey.strategy";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -19,13 +21,19 @@ import { SettingsModule } from 'src/settings/settings.module';
|
|||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
secret: configService.get('JWT_SECRET'),
|
secret: configService.get("JWT_SECRET"),
|
||||||
signOptions: { expiresIn: '1h' },
|
signOptions: { expiresIn: "365d" },
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
LocalStrategy,
|
||||||
|
JwtStrategy,
|
||||||
|
GoogleStrategy,
|
||||||
|
HeaderApiKeyStrategy,
|
||||||
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from "../users/users.service";
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from "bcryptjs";
|
||||||
import PayloadInterface from './interface/payload.interface';
|
import PayloadInterface from "./interface/payload.interface";
|
||||||
|
import { User } from "src/models/user";
|
||||||
|
import { MailerService } from "@nestjs-modules/mailer";
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private userService: UsersService,
|
private userService: UsersService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
|
private emailService: MailerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
validateApiKey(apikey: string): boolean {
|
||||||
|
if (process.env.API_KEYS == null) return false;
|
||||||
|
const keys = process.env.API_KEYS.split(",");
|
||||||
|
return keys.includes(apikey);
|
||||||
|
}
|
||||||
|
|
||||||
async validateUser(
|
async validateUser(
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<PayloadInterface | null> {
|
): Promise<PayloadInterface | null> {
|
||||||
const user = await this.userService.user({ username });
|
const user = await this.userService.user({ username });
|
||||||
if (user && bcrypt.compareSync(password, user.password)) {
|
if (user && user.password && bcrypt.compareSync(password, user.password)) {
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -31,4 +40,70 @@ export class AuthService {
|
|||||||
access_token,
|
access_token,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendVerifyMail(user: User) {
|
||||||
|
if (process.env.IGNORE_MAILS === "true") return;
|
||||||
|
if (user.email == null) return;
|
||||||
|
console.log("Sending verification mail to", user.email);
|
||||||
|
const token = await this.jwtService.signAsync(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{ expiresIn: "10h" },
|
||||||
|
);
|
||||||
|
await this.emailService.sendMail({
|
||||||
|
to: user.email,
|
||||||
|
from: "chromacase@octohub.app",
|
||||||
|
subject: "Mail verification for Chromacase",
|
||||||
|
html: `To verify your mail, please click on this <a href="${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendPasswordResetMail(user: User) {
|
||||||
|
if (process.env.IGNORE_MAILS === "true") return;
|
||||||
|
if (user.email == null) return;
|
||||||
|
console.log("Sending password reset mail to", user.email);
|
||||||
|
const token = await this.jwtService.signAsync(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{ expiresIn: "10h" },
|
||||||
|
);
|
||||||
|
await this.emailService.sendMail({
|
||||||
|
to: user.email,
|
||||||
|
from: "chromacase@octohub.app",
|
||||||
|
subject: "Password reset for Chromacase",
|
||||||
|
html: `To reset your password, please click on this <a href="${process.env.PUBLIC_URL}/password_reset?token=${token}">link</a>.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePassword(new_password: string, token: string): Promise<boolean> {
|
||||||
|
let verified;
|
||||||
|
try {
|
||||||
|
verified = await this.jwtService.verifyAsync(token);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Password reset token failure", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(verified);
|
||||||
|
await this.userService.updateUser({
|
||||||
|
where: { id: verified.userId },
|
||||||
|
data: { password: new_password },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyMail(userId: number, token: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.jwtService.verifyAsync(token);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Verify mail token failure", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.userService.updateUser({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { emailVerified: true },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
back/src/auth/chroma-auth.guard.ts
Normal file
22
back/src/auth/chroma-auth.guard.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { IS_PUBLIC_KEY } from "./public";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ChromaAuthGuard extends AuthGuard(["jwt", "api-key"]) {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Constants {
|
export class Constants {
|
||||||
constructor(private configService: ConfigService) {}
|
constructor(private configService: ConfigService) {}
|
||||||
|
|
||||||
getSecret = () => {
|
getSecret = () => {
|
||||||
return this.configService.get('JWT_SECRET');
|
return this.configService.get("JWT_SECRET");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
8
back/src/auth/dto/password_reset.dto .ts
Normal file
8
back/src/auth/dto/password_reset.dto .ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class PasswordResetDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class Profile {
|
export class Profile {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class RegisterDto {
|
export class RegisterDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
35
back/src/auth/google.strategy.ts
Normal file
35
back/src/auth/google.strategy.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
|
import { Strategy, VerifyCallback } from "passport-google-oauth20";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.GOOGLE_SECRET,
|
||||||
|
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||||
|
scope: ["email", "profile"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(
|
||||||
|
_accessToken: string,
|
||||||
|
_refreshToken: string,
|
||||||
|
profile: any,
|
||||||
|
done: VerifyCallback,
|
||||||
|
): Promise<any> {
|
||||||
|
const user = {
|
||||||
|
email: profile.emails[0].value,
|
||||||
|
username: profile.displayName,
|
||||||
|
password: null,
|
||||||
|
googleID: profile.id,
|
||||||
|
// firstName: name.givenName,
|
||||||
|
// lastName: name.familyName,
|
||||||
|
// picture: photos[0].value,
|
||||||
|
};
|
||||||
|
done(null, user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { IS_PUBLIC_KEY } from "./public";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
@@ -9,7 +9,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: configService.get('JWT_SECRET'),
|
secretOrKey: configService.get("JWT_SECRET"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
export class LocalAuthGuard extends AuthGuard("local") {}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Strategy } from 'passport-local';
|
import { Strategy } from "passport-local";
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from "./auth.service";
|
||||||
import PayloadInterface from './interface/payload.interface';
|
import PayloadInterface from "./interface/payload.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class JwtToken {
|
export class JwtToken {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
4
back/src/auth/public.ts
Normal file
4
back/src/auth/public.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from "@nestjs/common";
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = "isPublic";
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class CreateGenreDto {
|
export class CreateGenreDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -13,20 +13,30 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
} from '@nestjs/common';
|
UseGuards,
|
||||||
import { Plage } from 'src/models/plage';
|
} from "@nestjs/common";
|
||||||
import { CreateGenreDto } from './dto/create-genre.dto';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { Request } from 'express';
|
import { CreateGenreDto } from "./dto/create-genre.dto";
|
||||||
import { GenreService } from './genre.service';
|
import { Request } from "express";
|
||||||
import { Prisma, Genre } from '@prisma/client';
|
import { GenreService } from "./genre.service";
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { Prisma, Genre } from "@prisma/client";
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { ApiTags } from "@nestjs/swagger";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { createReadStream, existsSync } from "fs";
|
||||||
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
|
import { Genre as _Genre } from "src/_gen/prisma-class/genre";
|
||||||
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
|
import { Public } from "src/auth/public";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
@Controller('genre')
|
@Controller("genre")
|
||||||
@ApiTags('genre')
|
@ApiTags("genre")
|
||||||
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class GenreController {
|
export class GenreController {
|
||||||
static filterableFields: string[] = ['+id', 'name'];
|
static filterableFields: string[] = ["+id", "name"];
|
||||||
|
static includableFields: IncludeMap<Prisma.GenreInclude> = {
|
||||||
|
Song: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private readonly service: GenreService) {}
|
constructor(private readonly service: GenreService) {}
|
||||||
|
|
||||||
@@ -39,22 +49,23 @@ export class GenreController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.service.delete({ id });
|
return await this.service.delete({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/illustration')
|
@Get(":id/illustration")
|
||||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
@Public()
|
||||||
|
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||||
const genre = await this.service.get({ id });
|
const genre = await this.service.get({ id });
|
||||||
if (!genre) throw new NotFoundException('Genre not found');
|
if (!genre) throw new NotFoundException("Genre not found");
|
||||||
const path = `/assets/genres/${genre.name}/illustration.png`;
|
const path = `/assets/genres/${genre.name}/illustration.png`;
|
||||||
if (!existsSync(path))
|
if (!existsSync(path))
|
||||||
throw new NotFoundException('Illustration not found');
|
throw new NotFoundException("Illustration not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(path);
|
const file = createReadStream(path);
|
||||||
@@ -65,26 +76,36 @@ export class GenreController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOkResponsePlaginated(_Genre)
|
||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(GenreController.filterableFields)
|
@FilterQuery(GenreController.filterableFields)
|
||||||
where: Prisma.GenreWhereInput,
|
where: Prisma.GenreWhereInput,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("include") include: string,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Genre>> {
|
): Promise<Plage<Genre>> {
|
||||||
const ret = await this.service.list({
|
const ret = await this.service.list({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where,
|
where,
|
||||||
|
include: mapInclude(include, req, GenreController.includableFields),
|
||||||
});
|
});
|
||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
async findOne(
|
||||||
const res = await this.service.get({ id });
|
@Req() req: Request,
|
||||||
|
@Query("include") include: string,
|
||||||
|
@Param("id", ParseIntPipe) id: number,
|
||||||
|
) {
|
||||||
|
const res = await this.service.get(
|
||||||
|
{ id },
|
||||||
|
mapInclude(include, req, GenreController.includableFields),
|
||||||
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Genre not found');
|
if (res === null) throw new NotFoundException("Genre not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { GenreController } from './genre.controller';
|
import { GenreController } from "./genre.controller";
|
||||||
import { GenreService } from './genre.service';
|
import { GenreService } from "./genre.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, Genre } from '@prisma/client';
|
import { Prisma, Genre } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GenreService {
|
export class GenreService {
|
||||||
@@ -12,9 +12,13 @@ export class GenreService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(where: Prisma.GenreWhereUniqueInput): Promise<Genre | null> {
|
async get(
|
||||||
|
where: Prisma.GenreWhereUniqueInput,
|
||||||
|
include?: Prisma.GenreInclude,
|
||||||
|
): Promise<Genre | null> {
|
||||||
return this.prisma.genre.findUnique({
|
return this.prisma.genre.findUnique({
|
||||||
where,
|
where,
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,14 +28,16 @@ export class GenreService {
|
|||||||
cursor?: Prisma.GenreWhereUniqueInput;
|
cursor?: Prisma.GenreWhereUniqueInput;
|
||||||
where?: Prisma.GenreWhereInput;
|
where?: Prisma.GenreWhereInput;
|
||||||
orderBy?: Prisma.GenreOrderByWithRelationInput;
|
orderBy?: Prisma.GenreOrderByWithRelationInput;
|
||||||
|
include?: Prisma.GenreInclude;
|
||||||
}): Promise<Genre[]> {
|
}): Promise<Genre[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { skip, take, cursor, where, orderBy, include } = params;
|
||||||
return this.prisma.genre.findMany({
|
return this.prisma.genre.findMany({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ export class SongHistoryDto {
|
|||||||
score: number;
|
score: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
difficulties: Record<string, number>
|
difficulties: Record<string, number>;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
info: Record<string, number>
|
info: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,57 +9,82 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Request,
|
Request,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
|
import {
|
||||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
ApiCreatedResponse,
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
ApiOkResponse,
|
||||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
ApiOperation,
|
||||||
import { HistoryService } from './history.service';
|
ApiTags,
|
||||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
ApiUnauthorizedResponse,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { SearchHistory, SongHistory } from "@prisma/client";
|
||||||
|
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||||
|
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||||
|
import { HistoryService } from "./history.service";
|
||||||
|
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||||
|
import { SongHistory as _SongHistory } from "src/_gen/prisma-class/song_history";
|
||||||
|
import { SearchHistory as _SearchHistory } from "src/_gen/prisma-class/search_history";
|
||||||
|
import { SongController } from "src/song/song.controller";
|
||||||
|
import { mapInclude } from "src/utils/include";
|
||||||
|
|
||||||
@Controller('history')
|
@Controller("history")
|
||||||
@ApiTags('history')
|
@ApiTags("history")
|
||||||
export class HistoryController {
|
export class HistoryController {
|
||||||
constructor(private readonly historyService: HistoryService) { }
|
constructor(private readonly historyService: HistoryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@ApiOperation({ description: "Get song history of connected user" })
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiOkResponse({ type: _SongHistory, isArray: true })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async getHistory(
|
async getHistory(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
|
@Query("include") include: string,
|
||||||
): Promise<SongHistory[]> {
|
): Promise<SongHistory[]> {
|
||||||
return this.historyService.getHistory(req.user.id, { skip, take });
|
return this.historyService.getHistory(
|
||||||
|
req.user.id,
|
||||||
|
{ skip, take },
|
||||||
|
mapInclude(include, req, SongController.includableFields),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('search')
|
@Get("search")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@ApiOperation({ description: "Get search history of connected user" })
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiOkResponse({ type: _SearchHistory, isArray: true })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async getSearchHistory(
|
async getSearchHistory(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<SearchHistory[]> {
|
): Promise<SearchHistory[]> {
|
||||||
return this.historyService.getSearchHistory(req.user.id, { skip, take });
|
return this.historyService.getSearchHistory(req.user.id, { skip, take });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@HttpCode(201)
|
@HttpCode(201)
|
||||||
|
@ApiOperation({ description: "Create a record of a song played by a user" })
|
||||||
|
@ApiCreatedResponse({ description: "Succesfully created a record" })
|
||||||
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
|
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
|
||||||
return this.historyService.createSongHistoryRecord(record);
|
return this.historyService.createSongHistoryRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("search")
|
@Post("search")
|
||||||
@HttpCode(201)
|
@HttpCode(201)
|
||||||
|
@ApiOperation({ description: "Creates a search record in the users history" })
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiUnauthorizedResponse({description: "Invalid token"})
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async createSearchHistory(
|
async createSearchHistory(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() record: SearchHistoryDto
|
@Body() record: SearchHistoryDto,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.historyService.createSearchHistoryRecord(req.user.id, { query: record.query, type: record.type });
|
await this.historyService.createSearchHistoryRecord(req.user.id, {
|
||||||
}
|
query: record.query,
|
||||||
|
type: record.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { HistoryService } from './history.service';
|
import { HistoryService } from "./history.service";
|
||||||
import { HistoryController } from './history.controller';
|
import { HistoryController } from "./history.controller";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { HistoryService } from './history.service';
|
import { HistoryService } from "./history.service";
|
||||||
|
|
||||||
describe('HistoryService', () => {
|
describe("HistoryService", () => {
|
||||||
let service: HistoryService;
|
let service: HistoryService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [HistoryService],
|
providers: [HistoryService],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<HistoryService>(HistoryService);
|
service = module.get<HistoryService>(HistoryService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it("should be defined", () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
import { Prisma, SearchHistory, SongHistory } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HistoryService {
|
export class HistoryService {
|
||||||
constructor(private prisma: PrismaService) { }
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
async createSongHistoryRecord({
|
async createSongHistoryRecord({
|
||||||
songID,
|
songID,
|
||||||
@@ -45,12 +45,14 @@ export class HistoryService {
|
|||||||
async getHistory(
|
async getHistory(
|
||||||
playerId: number,
|
playerId: number,
|
||||||
{ skip, take }: { skip?: number; take?: number },
|
{ skip, take }: { skip?: number; take?: number },
|
||||||
|
include?: Prisma.SongInclude,
|
||||||
): Promise<SongHistory[]> {
|
): Promise<SongHistory[]> {
|
||||||
return this.prisma.songHistory.findMany({
|
return this.prisma.songHistory.findMany({
|
||||||
where: { user: { id: playerId } },
|
where: { user: { id: playerId } },
|
||||||
orderBy: { playDate: 'desc' },
|
orderBy: { playDate: "desc" },
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
|
include: { song: include ? { include } : true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export class HistoryService {
|
|||||||
}): Promise<{ best: number; history: SongHistory[] }> {
|
}): Promise<{ best: number; history: SongHistory[] }> {
|
||||||
const history = await this.prisma.songHistory.findMany({
|
const history = await this.prisma.songHistory.findMany({
|
||||||
where: { user: { id: playerId }, song: { id: songId } },
|
where: { user: { id: playerId }, song: { id: songId } },
|
||||||
orderBy: { playDate: 'desc' },
|
orderBy: { playDate: "desc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -74,7 +76,7 @@ export class HistoryService {
|
|||||||
|
|
||||||
async createSearchHistoryRecord(
|
async createSearchHistoryRecord(
|
||||||
userID: number,
|
userID: number,
|
||||||
{ query, type }: SearchHistoryDto
|
{ query, type }: SearchHistoryDto,
|
||||||
): Promise<SearchHistory> {
|
): Promise<SearchHistory> {
|
||||||
return this.prisma.searchHistory.create({
|
return this.prisma.searchHistory.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -95,7 +97,7 @@ export class HistoryService {
|
|||||||
): Promise<SearchHistory[]> {
|
): Promise<SearchHistory[]> {
|
||||||
return this.prisma.searchHistory.findMany({
|
return this.prisma.searchHistory.findMany({
|
||||||
where: { user: { id: playerId } },
|
where: { user: { id: playerId } },
|
||||||
orderBy: { searchDate: 'desc' },
|
orderBy: { searchDate: "desc" },
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
Request,
|
|
||||||
Param,
|
Param,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
DefaultValuePipe,
|
DefaultValuePipe,
|
||||||
@@ -12,12 +11,18 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Delete,
|
Delete,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
UseGuards,
|
||||||
import { Plage } from 'src/models/plage';
|
} from "@nestjs/common";
|
||||||
import { LessonService } from './lesson.service';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
import { LessonService } from "./lesson.service";
|
||||||
import { Prisma, Skill } from '@prisma/client';
|
import { ApiOperation, ApiProperty, ApiTags } from "@nestjs/swagger";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { Prisma, Skill } from "@prisma/client";
|
||||||
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
|
import { Lesson as _Lesson } from "src/_gen/prisma-class/lesson";
|
||||||
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
export class Lesson {
|
export class Lesson {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -32,49 +37,63 @@ export class Lesson {
|
|||||||
mainSkill: Skill;
|
mainSkill: Skill;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiTags('lessons')
|
@ApiTags("lessons")
|
||||||
@Controller('lesson')
|
@Controller("lesson")
|
||||||
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class LessonController {
|
export class LessonController {
|
||||||
static filterableFields: string[] = [
|
static filterableFields: string[] = [
|
||||||
'+id',
|
"+id",
|
||||||
'name',
|
"name",
|
||||||
'+requiredLevel',
|
"+requiredLevel",
|
||||||
'mainSkill',
|
"mainSkill",
|
||||||
];
|
];
|
||||||
|
static includableFields: IncludeMap<Prisma.LessonInclude> = {
|
||||||
|
LessonHistory: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private lessonService: LessonService) {}
|
constructor(private lessonService: LessonService) {}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get all lessons',
|
summary: "Get all lessons",
|
||||||
})
|
})
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOkResponsePlaginated(_Lesson)
|
||||||
async getAll(
|
async getAll(
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@FilterQuery(LessonController.filterableFields)
|
@FilterQuery(LessonController.filterableFields)
|
||||||
where: Prisma.LessonWhereInput,
|
where: Prisma.LessonWhereInput,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("include") include: string,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Lesson>> {
|
): Promise<Plage<Lesson>> {
|
||||||
const ret = await this.lessonService.getAll({
|
const ret = await this.lessonService.getAll({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where,
|
where,
|
||||||
|
include: mapInclude(include, request, LessonController.includableFields),
|
||||||
});
|
});
|
||||||
return new Plage(ret, request);
|
return new Plage(ret, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get a particular lessons',
|
summary: "Get a particular lessons",
|
||||||
})
|
})
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
async get(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
async get(
|
||||||
const ret = await this.lessonService.get(id);
|
@Req() req: Request,
|
||||||
|
@Query("include") include: string,
|
||||||
|
@Param("id", ParseIntPipe) id: number,
|
||||||
|
): Promise<Lesson> {
|
||||||
|
const ret = await this.lessonService.get(
|
||||||
|
id,
|
||||||
|
mapInclude(include, req, LessonController.includableFields),
|
||||||
|
);
|
||||||
if (!ret) throw new NotFoundException();
|
if (!ret) throw new NotFoundException();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Create a lessons',
|
summary: "Create a lessons",
|
||||||
})
|
})
|
||||||
@Post()
|
@Post()
|
||||||
async post(@Body() lesson: Lesson): Promise<Lesson> {
|
async post(@Body() lesson: Lesson): Promise<Lesson> {
|
||||||
@@ -87,10 +106,10 @@ export class LessonController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Delete a lessons',
|
summary: "Delete a lessons",
|
||||||
})
|
})
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
async delete(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
async delete(@Param("id", ParseIntPipe) id: number): Promise<Lesson> {
|
||||||
try {
|
try {
|
||||||
return await this.lessonService.delete(id);
|
return await this.lessonService.delete(id);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { LessonController } from './lesson.controller';
|
import { LessonController } from "./lesson.controller";
|
||||||
import { LessonService } from './lesson.service';
|
import { LessonService } from "./lesson.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { LessonService } from './lesson.service';
|
import { LessonService } from "./lesson.service";
|
||||||
|
|
||||||
describe('LessonService', () => {
|
describe("LessonService", () => {
|
||||||
let service: LessonService;
|
let service: LessonService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -12,7 +12,7 @@ describe('LessonService', () => {
|
|||||||
service = module.get<LessonService>(LessonService);
|
service = module.get<LessonService>(LessonService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it("should be defined", () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Lesson, Prisma } from '@prisma/client';
|
import { Lesson, Prisma } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LessonService {
|
export class LessonService {
|
||||||
@@ -12,22 +12,28 @@ export class LessonService {
|
|||||||
cursor?: Prisma.LessonWhereUniqueInput;
|
cursor?: Prisma.LessonWhereUniqueInput;
|
||||||
where?: Prisma.LessonWhereInput;
|
where?: Prisma.LessonWhereInput;
|
||||||
orderBy?: Prisma.LessonOrderByWithRelationInput;
|
orderBy?: Prisma.LessonOrderByWithRelationInput;
|
||||||
|
include?: Prisma.LessonInclude;
|
||||||
}): Promise<Lesson[]> {
|
}): Promise<Lesson[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { skip, take, cursor, where, orderBy, include } = params;
|
||||||
return this.prisma.lesson.findMany({
|
return this.prisma.lesson.findMany({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: number): Promise<Lesson | null> {
|
async get(
|
||||||
|
id: number,
|
||||||
|
include?: Prisma.LessonInclude,
|
||||||
|
): Promise<Lesson | null> {
|
||||||
return this.prisma.lesson.findFirst({
|
return this.prisma.lesson.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
|
include,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,72 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from "@nestjs/core";
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from "./app.module";
|
||||||
import { PrismaService } from './prisma/prisma.service';
|
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import {
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ValidationPipe,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { RequestLogger, RequestLoggerOptions } from "json-logger-service";
|
||||||
|
import { tap } from "rxjs";
|
||||||
|
import { PrismaModel } from "./_gen/prisma-class";
|
||||||
|
import { PrismaService } from "./prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AspectLogger implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler) {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const res = context.switchToHttp().getResponse();
|
||||||
|
const { statusCode } = context.switchToHttp().getResponse();
|
||||||
|
const { originalUrl, method, params, query, body, user } = req;
|
||||||
|
|
||||||
|
const toPrint = {
|
||||||
|
originalUrl,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
query,
|
||||||
|
body,
|
||||||
|
userId: user?.id ?? "not logged in",
|
||||||
|
username: user?.username ?? "not logged in",
|
||||||
|
};
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap((/* data */) =>
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
...toPrint,
|
||||||
|
statusCode,
|
||||||
|
//data, //TODO: Data crashed with images
|
||||||
|
}),
|
||||||
|
),),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
const prismaService = app.get(PrismaService);
|
app.use(
|
||||||
await prismaService.enableShutdownHooks(app);
|
RequestLogger.buildExpressRequestLogger({
|
||||||
|
doNotLogPaths: ["/health"],
|
||||||
|
} as RequestLoggerOptions),
|
||||||
|
);
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('Chromacase')
|
.setTitle("Chromacase")
|
||||||
.setDescription('The chromacase API')
|
.setDescription("The chromacase API")
|
||||||
.setVersion('1.0')
|
.setVersion("1.0")
|
||||||
.build();
|
.build();
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
const document = SwaggerModule.createDocument(app, config, {
|
||||||
SwaggerModule.setup('api', app, document);
|
extraModels: [...PrismaModel.extraModels],
|
||||||
|
});
|
||||||
|
SwaggerModule.setup("api", app, document);
|
||||||
|
|
||||||
app.useGlobalPipes(new ValidationPipe());
|
app.useGlobalPipes(new ValidationPipe());
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
|
//app.useGlobalInterceptors(new AspectLogger());
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -2,23 +2,42 @@
|
|||||||
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
|
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { Type, applyDecorators } from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiExtraModels,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiProperty,
|
||||||
|
getSchemaPath,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
|
||||||
export class Plage<T> {
|
export class PlageMetadata {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
metadata: {
|
this: string;
|
||||||
this: string;
|
@ApiProperty({
|
||||||
next: string | null;
|
type: "string",
|
||||||
previous: string | null;
|
nullable: true,
|
||||||
};
|
description: "null if there is no next page, couldn't set it in swagger",
|
||||||
|
})
|
||||||
|
next: string | null;
|
||||||
|
@ApiProperty({
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"null if there is no previous page, couldn't set it in swagger",
|
||||||
|
})
|
||||||
|
previous: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Plage<T extends object> {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
metadata: PlageMetadata;
|
||||||
data: T[];
|
data: T[];
|
||||||
|
|
||||||
constructor(data: T[], request: Request | any) {
|
constructor(data: T[], request: Request | any) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
let take = Number(request.query['take'] ?? 20).valueOf();
|
let take = Number(request.query["take"] ?? 20).valueOf();
|
||||||
if (take == 0) take = 20;
|
if (take == 0) take = 20;
|
||||||
let skipped: number = Number(request.query['skip'] ?? 0).valueOf();
|
let skipped: number = Number(request.query["skip"] ?? 0).valueOf();
|
||||||
if (skipped % take) {
|
if (skipped % take) {
|
||||||
skipped += take - (skipped % take);
|
skipped += take - (skipped % take);
|
||||||
}
|
}
|
||||||
@@ -49,3 +68,25 @@ export class Plage<T> {
|
|||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
|
||||||
|
dataDto: DataDto,
|
||||||
|
) =>
|
||||||
|
applyDecorators(
|
||||||
|
ApiExtraModels(Plage, dataDto),
|
||||||
|
ApiOkResponse({
|
||||||
|
schema: {
|
||||||
|
allOf: [
|
||||||
|
{ $ref: getSchemaPath(Plage) },
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: "array",
|
||||||
|
items: { $ref: getSchemaPath(dataDto) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class Setting {
|
export class Setting {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -6,9 +6,11 @@ export class User {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
username: string;
|
username: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
email: string;
|
email: string | null;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
partyPlayed: number;
|
partyPlayed: number;
|
||||||
|
@ApiProperty()
|
||||||
|
totalScore: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from "./prisma.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PrismaService],
|
providers: [PrismaService],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from "./prisma.service";
|
||||||
|
|
||||||
describe('PrismaService', () => {
|
describe("PrismaService", () => {
|
||||||
let service: PrismaService;
|
let service: PrismaService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -12,7 +12,7 @@ describe('PrismaService', () => {
|
|||||||
service = module.get<PrismaService>(PrismaService);
|
service = module.get<PrismaService>(PrismaService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it("should be defined", () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
|
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect();
|
await this.$connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
async enableShutdownHooks(app: INestApplication) {
|
|
||||||
this.$on('beforeExit', async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
22
back/src/scores/scores.controller.ts
Normal file
22
back/src/scores/scores.controller.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Controller, Get, Put } from "@nestjs/common";
|
||||||
|
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { ScoresService } from "./scores.service";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
@ApiTags("scores")
|
||||||
|
@Controller("scores")
|
||||||
|
export class ScoresController {
|
||||||
|
constructor(private readonly scoresService: ScoresService) {}
|
||||||
|
|
||||||
|
@ApiOkResponse({ description: "Successfully sent the Top 20 players" })
|
||||||
|
@Get("top/20")
|
||||||
|
getTopTwenty(): Promise<User[]> {
|
||||||
|
return this.scoresService.topTwenty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ApiOkResponse{{description: "Successfully updated the user's total score"}}
|
||||||
|
// @Put("/add")
|
||||||
|
// addScore(): Promise<void> {
|
||||||
|
// return this.ScoresService.add()
|
||||||
|
// }
|
||||||
|
}
|
||||||
11
back/src/scores/scores.module.ts
Normal file
11
back/src/scores/scores.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { ScoresService } from "./scores.service";
|
||||||
|
import { ScoresController } from "./scores.controller";
|
||||||
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [ScoresController],
|
||||||
|
providers: [ScoresService],
|
||||||
|
})
|
||||||
|
export class ScoresModule {}
|
||||||
17
back/src/scores/scores.service.ts
Normal file
17
back/src/scores/scores.service.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ScoresService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async topTwenty(): Promise<User[]> {
|
||||||
|
return this.prisma.user.findMany({
|
||||||
|
orderBy: {
|
||||||
|
totalScore: "desc",
|
||||||
|
},
|
||||||
|
take: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class SearchSongDto {
|
export class SearchSongDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user