Compare commits
489 Commits
feature/ad
...
clem-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae0bbea3a | ||
|
|
f5136ae59b | ||
|
|
f632ed42a3 | ||
|
|
00ee5cd531 | ||
|
|
1d496301d9 | ||
|
|
d44e75a83a | ||
| e487d6d91e | |||
| 7a63a66da5 | |||
| 17f64cd849 | |||
| ec17aa741f | |||
|
|
358841abd5 | ||
|
|
64e7dbc71e | ||
|
|
5a0809c1d0 | ||
|
|
4b5e3d2b04 | ||
|
|
5f24c6e7bd | ||
|
|
8bdf8ce334 | ||
|
|
9012a6a9d8 | ||
|
|
c5fd4aa7d5 | ||
|
|
65cd04a494 | ||
|
|
c79ae7c6e8 | ||
|
|
ddc97f0923 | ||
|
|
a9b902a427 | ||
|
|
96d8e649c8 | ||
|
|
22c93b7571 | ||
|
|
0644d4b580 | ||
|
|
ee6a76cdd9 | ||
|
|
5ba815590a | ||
|
|
dd09827d08 | ||
| b5b94adc83 | |||
| 3c04e8bb39 | |||
| 17a4328af5 | |||
| e81f2c1f75 | |||
| f77874bec4 | |||
| cfc72b8bc1 | |||
| 359b20fc6d | |||
| a3659618ea | |||
| fa60fc65a9 | |||
| b1727b7838 | |||
| a3f4703dae | |||
| 038918c212 | |||
| 42a947dfb0 | |||
| 5525110d39 | |||
| 7160b77607 | |||
| b5183f84b4 | |||
|
|
13050e52f9 | ||
|
|
5ef3885f72 | ||
|
|
a103666caf | ||
|
|
29da5c2788 | ||
|
|
1880b89b0c | ||
|
|
e769ff1f13 | ||
|
|
9e7873cdd7 | ||
|
|
f46c2cfb4a | ||
|
|
9f14061efd | ||
|
|
851ee7420f | ||
|
|
ef57eb752d | ||
|
|
fcb29ae484 | ||
|
|
5c4847ae2c | ||
|
|
60a73781bd | ||
|
|
4e3b378d6a | ||
|
|
2bf1e783a9 | ||
|
|
375d36f6c5 | ||
|
|
495380ec43 | ||
|
|
af0531bb0c | ||
|
|
c5124fa6ad | ||
|
|
962cf58e77 | ||
|
|
60988dd599 | ||
|
|
004a541302 | ||
|
|
f4cd9e18ea | ||
|
|
2dc301addf | ||
|
|
e85a959c26 | ||
|
|
339e808d27 | ||
|
|
22d1a97abd | ||
|
|
ce4baa61dc | ||
|
|
e90c7f05a8 | ||
|
|
fb0e43af88 | ||
|
|
4577997b1c | ||
|
|
9bb256f2ee | ||
|
|
d3994ff26e | ||
|
|
00d097f643 | ||
|
|
99da77f23e | ||
|
|
7a6dc8b0c9 | ||
|
|
b4f04f9b71 | ||
|
|
9df0c98100 | ||
|
|
a47f8744f8 | ||
| 80329e240e | |||
| 70b109e78b | |||
| a6a96d6a1e | |||
| cc4b69ca50 | |||
|
|
e733c6acc8 | ||
|
|
afa6f421d3 | ||
|
|
7d7f886661 | ||
|
|
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 | ||
|
|
c81f8df61c | ||
|
|
a3676fabf8 | ||
|
|
dc398d6e06 | ||
|
|
d5da112a01 | ||
|
|
96048bd671 | ||
|
|
dcdc6b196d | ||
|
|
9f542fc9dd | ||
| 930191569f | |||
| 74cd9c0df2 | |||
| d2642b4fb8 | |||
| ebcc48cc57 | |||
| 95b08935cc | |||
| 04487c9b24 | |||
|
|
20eb62d19b | ||
|
|
567d3250e2 | ||
| 4207d5ee50 | |||
|
|
509cc5b9f8 | ||
|
|
1b22dba9cd | ||
|
|
2ec95dd3c3 | ||
|
|
c0d9ee7ca6 | ||
|
|
27f7945289 | ||
|
|
5a190f3b96 | ||
|
|
3d76834f45 | ||
| ccc86895e2 | |||
| 279d16d59a | |||
| 04d288b844 |
@@ -8,6 +8,6 @@ insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = tab
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
[{*.yaml,*.yml,*.nix}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
17
.env.example
@@ -7,4 +7,19 @@ JWT_SECRET=wow
|
||||
POSTGRES_DB=chromacase
|
||||
API_URL=http://localhost:80/api
|
||||
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
@@ -1,4 +1 @@
|
||||
if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
|
||||
fi
|
||||
use flake
|
||||
use nix
|
||||
|
||||
1
.git-blame-ignore-revs
Normal file
@@ -0,0 +1 @@
|
||||
14e241db37c4080bc0bd87363cf7a57ef8379f46
|
||||
152
.github/workflows/CI.yml
vendored
@@ -1,151 +1,18 @@
|
||||
name: CI
|
||||
|
||||
name: Deploy
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
- main
|
||||
|
||||
jobs:
|
||||
|
||||
## Build Back ##
|
||||
|
||||
Build_Back:
|
||||
deployment:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
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
|
||||
if: github.event.pull_request.merged == true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -190,6 +57,7 @@ jobs:
|
||||
build-args: |
|
||||
API_URL=${{secrets.API_URL}}
|
||||
SCORO_URL=${{secrets.SCORO_URL}}
|
||||
|
||||
- name: Docker meta scorometer
|
||||
id: meta_scorometer
|
||||
uses: docker/metadata-action@v4
|
||||
|
||||
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
@@ -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
@@ -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
@@ -13,3 +13,7 @@ log.html
|
||||
node_modules/
|
||||
./front/coverage
|
||||
.venv
|
||||
.data
|
||||
.DS_Store
|
||||
_gen
|
||||
venv
|
||||
|
||||
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.
|
||||
|
||||
Ç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
@@ -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
|
After Width: | Height: | Size: 597 KiB |
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
BIN
assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3
Normal file
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
BIN
assets/musics/Canon in D (easy)/melody.mp3
Normal file
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
assets/musics/French National Anthem La Marseillaise/melody.mp3
Normal file
|
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 932 B |
BIN
assets/musics/Jesus Alegria dos Homens/melody.mp3
Normal file
BIN
assets/musics/Liebestraum (easy)/melody.mp3
Normal file
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
BIN
assets/musics/Mary, Did You Know/melody.mp3
Normal file
BIN
assets/musics/SCORO_TEST/melody.mp3
Normal file
BIN
assets/musics/Sarabande - William Gillock/melody.mp3
Normal file
BIN
assets/musics/Short/melody.mp3
Normal file
BIN
assets/musics/Silent Night/melody.mp3
Normal file
BIN
assets/musics/Twinkle Twinkle Little Star/melody.mp3
Normal file
@@ -1,12 +1,17 @@
|
||||
#!/bin/env python3
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
import requests
|
||||
import glob
|
||||
from mido import MidiFile
|
||||
from configparser import ConfigParser
|
||||
|
||||
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):
|
||||
if not name:
|
||||
@@ -14,27 +19,27 @@ def getOrCreateAlbum(name, artistId):
|
||||
res = requests.post(f"{url}/album", json={
|
||||
"name": name,
|
||||
"artist": artistId,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
return out["id"]
|
||||
|
||||
def getOrCreateGenre(names):
|
||||
ids = []
|
||||
for name in names.split(","):
|
||||
res = requests.post(f"{url}/genre", json={
|
||||
"name": name,
|
||||
})
|
||||
out = res.json()
|
||||
print(out)
|
||||
ids += [out["id"]]
|
||||
#TODO handle multiple genres
|
||||
return ids[0]
|
||||
ids = []
|
||||
for name in names.split(","):
|
||||
res = requests.post(f"{url}/genre", json={
|
||||
"name": name,
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
ids += [out["id"]]
|
||||
#TODO handle multiple genres
|
||||
return ids[0]
|
||||
|
||||
def getOrCreateArtist(name):
|
||||
res = requests.post(f"{url}/artist", json={
|
||||
"name": name,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
return out["id"]
|
||||
@@ -42,10 +47,13 @@ def getOrCreateArtist(name):
|
||||
def populateFile(path, midi, mxl):
|
||||
config = ConfigParser()
|
||||
config.read(path)
|
||||
mid = MidiFile(midi)
|
||||
metadata = config["Metadata"];
|
||||
difficulties = dict(config["Difficulties"])
|
||||
difficulties["length"] = round((mid.length), 2)
|
||||
artistId = getOrCreateArtist(metadata["Artist"])
|
||||
print(f"Populating {metadata['Name']}")
|
||||
print(auth_headers)
|
||||
res = requests.post(f"{url}/song", json={
|
||||
"name": metadata["Name"],
|
||||
"midiPath": f"/assets/{midi}",
|
||||
@@ -55,7 +63,7 @@ def populateFile(path, midi, mxl):
|
||||
"album": getOrCreateAlbum(metadata["Album"], artistId),
|
||||
"genre": getOrCreateGenre(metadata["Genre"]),
|
||||
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
|
||||
})
|
||||
}, headers=auth_headers)
|
||||
print(res.json())
|
||||
|
||||
|
||||
|
||||
2
assets/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
mido
|
||||
requests
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -5,4 +5,4 @@ RUN npm install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN npx prisma generate
|
||||
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
|
||||
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
|
||||
|
||||
18310
back/package-lock.json
generated
@@ -10,8 +10,8 @@
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:dev": "nest start --watch --preserveWatchOutput",
|
||||
"start:debug": "nest start --debug --watch --preserveWatchOutput",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
@@ -21,57 +21,74 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^8.0.0",
|
||||
"@nestjs/config": "^2.1.0",
|
||||
"@nestjs/core": "^8.0.0",
|
||||
"@nestjs/jwt": "^8.0.1",
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
"@nestjs/common": "^10.1.0",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.1.0",
|
||||
"@nestjs/jwt": "^10.1.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/passport": "^8.2.2",
|
||||
"@nestjs/platform-express": "^8.0.0",
|
||||
"@nestjs/swagger": "^5.2.1",
|
||||
"@prisma/client": "^4.4.0",
|
||||
"@nestjs/passport": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.1.0",
|
||||
"@nestjs/swagger": "^7.1.2",
|
||||
"@prisma/client": "^5.0.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/passport": "^1.0.9",
|
||||
"@types/passport": "^1.0.12",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"canvas": "^2.11.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"class-validator": "^0.14.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",
|
||||
"prisma-class-generator": "^0.2.7",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0",
|
||||
"swagger-ui-express": "^4.5.0"
|
||||
"rimraf": "^5.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-express": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^8.0.0",
|
||||
"@nestjs/schematics": "^8.0.0",
|
||||
"@nestjs/testing": "^8.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.2.5",
|
||||
"prettier": "^2.3.2",
|
||||
"prisma": "^4.4.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "^3.10.1",
|
||||
"typescript": "^4.3.5"
|
||||
"@nestjs/cli": "^10.1.10",
|
||||
"@nestjs/schematics": "^10.0.1",
|
||||
"@nestjs/testing": "^10.1.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "29.5.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.4.4",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.6.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
"ts",
|
||||
"mjs"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
generator prismaClassGenerator {
|
||||
provider = "prisma-class-generator"
|
||||
dryRun = false
|
||||
separateRelationFields = true
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
@@ -12,14 +18,27 @@ datasource db {
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
email String
|
||||
password String?
|
||||
email String? @unique
|
||||
emailVerified Boolean @default(false)
|
||||
googleID String? @unique
|
||||
isGuest Boolean @default(false)
|
||||
partyPlayed Int @default(0)
|
||||
totalScore Int @default(0)
|
||||
LessonHistory LessonHistory[]
|
||||
SongHistory SongHistory[]
|
||||
searchHistory SearchHistory[]
|
||||
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 {
|
||||
@@ -59,6 +78,7 @@ model Song {
|
||||
genre Genre? @relation(fields: [genreId], references: [id])
|
||||
difficulties Json
|
||||
SongHistory SongHistory[]
|
||||
likedByUsers LikedSongs[]
|
||||
}
|
||||
|
||||
model SongHistory {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
ConflictException,
|
||||
Controller,
|
||||
@@ -12,23 +11,36 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { Plage } from 'src/models/plage';
|
||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||
import { AlbumService } from './album.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Album } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateAlbumDto } from "./dto/create-album.dto";
|
||||
import { AlbumService } from "./album.service";
|
||||
import { Request } from "express";
|
||||
import { Prisma, Album } from "@prisma/client";
|
||||
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')
|
||||
@ApiTags('album')
|
||||
@Controller("album")
|
||||
@ApiTags("album")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
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) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
description: "Register a new album, should not be used by frontend",
|
||||
})
|
||||
async create(@Body() createAlbumDto: CreateAlbumDto) {
|
||||
try {
|
||||
return await this.albumService.createAlbum({
|
||||
@@ -44,36 +56,50 @@ export class AlbumController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "Delete an album by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.albumService.deleteAlbum({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponsePlaginated(_Album)
|
||||
@ApiOperation({ description: "Get all albums paginated" })
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(AlbumController.filterableFields)
|
||||
where: Prisma.AlbumWhereInput,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Album>> {
|
||||
const ret = await this.albumService.albums({
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, req, AlbumController.includableFields),
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const res = await this.albumService.album({ id });
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get an album by 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { AlbumController } from './album.controller';
|
||||
import { AlbumService } from './album.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { AlbumController } from "./album.controller";
|
||||
import { AlbumService } from "./album.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Album } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Album } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class AlbumService {
|
||||
@@ -14,9 +14,11 @@ export class AlbumService {
|
||||
|
||||
async album(
|
||||
albumWhereUniqueInput: Prisma.AlbumWhereUniqueInput,
|
||||
include?: Prisma.AlbumInclude,
|
||||
): Promise<Album | null> {
|
||||
return this.prisma.album.findUnique({
|
||||
where: albumWhereUniqueInput,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,14 +28,16 @@ export class AlbumService {
|
||||
cursor?: Prisma.AlbumWhereUniqueInput;
|
||||
where?: Prisma.AlbumWhereInput;
|
||||
orderBy?: Prisma.AlbumOrderByWithRelationInput;
|
||||
include?: Prisma.AlbumInclude;
|
||||
}): Promise<Album[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.album.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateAlbumDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
|
||||
describe('AppController', () => {
|
||||
describe("AppController", () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -14,9 +14,9 @@ describe('AppController', () => {
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
describe("root", () => {
|
||||
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 { AppService } from './app.service';
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { AppService } from "./app.service";
|
||||
import { ApiOkResponse } from "@nestjs/swagger";
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponse({
|
||||
description: "Return a hello world message, used as a health route",
|
||||
})
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PrismaService } from './prisma/prisma.service';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { SongModule } from './song/song.module';
|
||||
import { LessonModule } from './lesson/lesson.module';
|
||||
import { SettingsModule } from './settings/settings.module';
|
||||
import { ArtistService } from './artist/artist.service';
|
||||
import { GenreModule } from './genre/genre.module';
|
||||
import { ArtistModule } from './artist/artist.module';
|
||||
import { AlbumModule } from './album/album.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
import { HistoryModule } from './history/history.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
import { PrismaService } from "./prisma/prisma.service";
|
||||
import { UsersModule } from "./users/users.module";
|
||||
import { PrismaModule } from "./prisma/prisma.module";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { SongModule } from "./song/song.module";
|
||||
import { LessonModule } from "./lesson/lesson.module";
|
||||
import { SettingsModule } from "./settings/settings.module";
|
||||
import { ArtistService } from "./artist/artist.service";
|
||||
import { GenreModule } from "./genre/genre.module";
|
||||
import { ArtistModule } from "./artist/artist.module";
|
||||
import { AlbumModule } from "./album/album.module";
|
||||
import { SearchModule } from "./search/search.module";
|
||||
import { HistoryModule } from "./history/history.module";
|
||||
import { MailerModule } from "@nestjs-modules/mailer";
|
||||
import { ScoresModule } from "./scores/scores.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,6 +30,13 @@ import { HistoryModule } from './history/history.module';
|
||||
SearchModule,
|
||||
SettingsModule,
|
||||
HistoryModule,
|
||||
ScoresModule,
|
||||
MailerModule.forRoot({
|
||||
transport: process.env.SMTP_TRANSPORT,
|
||||
defaults: {
|
||||
from: process.env.MAIL_AUTHOR,
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, PrismaService, ArtistService],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
return "Hello World!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
ConflictException,
|
||||
Controller,
|
||||
@@ -14,24 +13,43 @@ import {
|
||||
Query,
|
||||
Req,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { Plage } from 'src/models/plage';
|
||||
import { CreateArtistDto } from './dto/create-artist.dto';
|
||||
import { Request } from 'express';
|
||||
import { ArtistService } from './artist.service';
|
||||
import { Prisma, Artist } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateArtistDto } from "./dto/create-artist.dto";
|
||||
import { Request } from "express";
|
||||
import { ArtistService } from "./artist.service";
|
||||
import { Prisma, Artist } from "@prisma/client";
|
||||
import {
|
||||
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')
|
||||
@ApiTags('artist')
|
||||
@Controller("artist")
|
||||
@ApiTags("artist")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
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) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
description: "Register a new artist, should not be used by frontend",
|
||||
})
|
||||
async create(@Body() dto: CreateArtistDto) {
|
||||
try {
|
||||
return await this.service.create(dto);
|
||||
@@ -40,22 +58,26 @@ export class ArtistController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "Delete an artist by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.service.delete({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
@Get(":id/illustration")
|
||||
@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 });
|
||||
if (!artist) throw new NotFoundException('Artist not found');
|
||||
if (!artist) throw new NotFoundException("Artist not found");
|
||||
const path = `/assets/artists/${artist.name}/illustration.png`;
|
||||
if (!existsSync(path))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(path);
|
||||
@@ -66,26 +88,39 @@ export class ArtistController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ description: "Get all artists paginated" })
|
||||
@ApiOkResponsePlaginated(_Artist)
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(ArtistController.filterableFields)
|
||||
where: Prisma.ArtistWhereInput,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Artist>> {
|
||||
const ret = await this.service.list({
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, req, ArtistController.includableFields),
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const res = await this.service.get({ id });
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get an artist by 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { ArtistController } from './artist.controller';
|
||||
import { ArtistService } from './artist.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { ArtistController } from "./artist.controller";
|
||||
import { ArtistService } from "./artist.service";
|
||||
import { SearchModule } from "src/search/search.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, SearchModule],
|
||||
controllers: [ArtistController],
|
||||
providers: [ArtistService],
|
||||
})
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Artist } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Artist } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { MeiliService } from "src/search/meilisearch.service";
|
||||
|
||||
@Injectable()
|
||||
export class ArtistService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private search: MeiliService,
|
||||
) {}
|
||||
|
||||
async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
|
||||
return this.prisma.artist.create({
|
||||
const ret = await this.prisma.artist.create({
|
||||
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({
|
||||
where,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,20 +34,24 @@ export class ArtistService {
|
||||
cursor?: Prisma.ArtistWhereUniqueInput;
|
||||
where?: Prisma.ArtistWhereInput;
|
||||
orderBy?: Prisma.ArtistOrderByWithRelationInput;
|
||||
include?: Prisma.ArtistInclude;
|
||||
}): Promise<Artist[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.artist.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
|
||||
return this.prisma.artist.delete({
|
||||
const ret = await this.prisma.artist.delete({
|
||||
where,
|
||||
});
|
||||
await this.search.index("artists").deleteDocument(ret.id);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateArtistDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
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
@@ -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
@@ -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,
|
||||
Delete,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
HttpCode,
|
||||
Put,
|
||||
InternalServerErrorException,
|
||||
Patch,
|
||||
NotFoundException,
|
||||
} 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';
|
||||
Req,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
HttpStatus,
|
||||
ParseFilePipeBuilder,
|
||||
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 {
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiConflictResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { User } from '../models/user';
|
||||
import { JwtToken } from './models/jwt';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { Profile } from './dto/profile.dto';
|
||||
import { Setting } from 'src/models/setting';
|
||||
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
|
||||
import { SettingsService } from 'src/settings/settings.service';
|
||||
} from "@nestjs/swagger";
|
||||
import { User } from "../models/user";
|
||||
import { JwtToken } from "./models/jwt";
|
||||
import { LoginDto } from "./dto/login.dto";
|
||||
import { Profile } from "./dto/profile.dto";
|
||||
import { Setting } from "src/models/setting";
|
||||
import { UpdateSettingDto } from "src/settings/dto/update-setting.dto";
|
||||
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')
|
||||
@Controller('auth')
|
||||
@ApiTags("auth")
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
@@ -42,49 +62,167 @@ export class AuthController {
|
||||
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> {
|
||||
try {
|
||||
const user = await this.usersService.createUser(registerDto)
|
||||
const user = await this.usersService.createUser(registerDto);
|
||||
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);
|
||||
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 })
|
||||
@HttpCode(200)
|
||||
@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> {
|
||||
return this.authService.login(req.user);
|
||||
}
|
||||
|
||||
@Post("guest")
|
||||
@HttpCode(200)
|
||||
@Post('guest')
|
||||
@ApiOperation({ description: "Login as a guest account" })
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||
async guest(): Promise<JwtToken> {
|
||||
const user = await this.usersService.createGuest();
|
||||
await this.settingsService.createUserSetting(user.id);
|
||||
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)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully logged in', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me')
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me")
|
||||
@ApiOperation({ description: "Get the user info of connected user" })
|
||||
async getProfile(@Request() req: any): Promise<User> {
|
||||
const user = await this.usersService.user({ id: req.user.id });
|
||||
if (!user) throw new InternalServerErrorException();
|
||||
return user;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully edited profile', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Put('me')
|
||||
@ApiOkResponse({ description: "Successfully edited profile", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Put("me")
|
||||
@ApiOperation({ description: "Edit the profile of connected user" })
|
||||
editProfile(
|
||||
@Request() req: any,
|
||||
@Body() profile: Partial<Profile>,
|
||||
@@ -107,35 +245,83 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully deleted', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Delete('me')
|
||||
@ApiOkResponse({ description: "Successfully deleted", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Delete("me")
|
||||
@ApiOperation({ description: "Delete the profile of connected user" })
|
||||
deleteSelf(@Request() req: any): Promise<User> {
|
||||
return this.usersService.deleteUser({ id: req.user.id });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
|
||||
@ApiUnauthorizedResponse({description: 'Invalid token'})
|
||||
@Patch('me/settings')
|
||||
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Patch("me/settings")
|
||||
@ApiOperation({ description: "Edit the settings of connected user" })
|
||||
udpateSettings(
|
||||
@Request() req: any,
|
||||
@Body() settingUserDto: UpdateSettingDto): Promise<Setting> {
|
||||
@Body() settingUserDto: UpdateSettingDto,
|
||||
): Promise<Setting> {
|
||||
return this.settingsService.updateUserSettings({
|
||||
where: { userId: +req.user.id},
|
||||
where: { userId: +req.user.id },
|
||||
data: settingUserDto,
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
|
||||
@ApiUnauthorizedResponse({description: 'Invalid token'})
|
||||
@Get('me/settings')
|
||||
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me/settings")
|
||||
@ApiOperation({ description: "Get the settings of connected user" })
|
||||
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();
|
||||
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 { UsersModule } from 'src/users/users.module';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { LocalStrategy } from './local.strategy';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { SettingsModule } from 'src/settings/settings.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UsersModule } from "src/users/users.module";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { LocalStrategy } from "./local.strategy";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtStrategy } from "./jwt.strategy";
|
||||
import { SettingsModule } from "src/settings/settings.module";
|
||||
import { GoogleStrategy } from "./google.strategy";
|
||||
import { HeaderApiKeyStrategy } from "./apikey.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -19,13 +21,19 @@ import { SettingsModule } from 'src/settings/settings.module';
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '1h' },
|
||||
secret: configService.get("JWT_SECRET"),
|
||||
signOptions: { expiresIn: "365d" },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
providers: [
|
||||
AuthService,
|
||||
LocalStrategy,
|
||||
JwtStrategy,
|
||||
GoogleStrategy,
|
||||
HeaderApiKeyStrategy,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import PayloadInterface from './interface/payload.interface';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import * as bcrypt from "bcryptjs";
|
||||
import PayloadInterface from "./interface/payload.interface";
|
||||
import { User } from "src/models/user";
|
||||
import { MailerService } from "@nestjs-modules/mailer";
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private userService: UsersService,
|
||||
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(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<PayloadInterface | null> {
|
||||
const user = await this.userService.user({ username });
|
||||
if (user && bcrypt.compareSync(password, user.password)) {
|
||||
if (user && user.password && bcrypt.compareSync(password, user.password)) {
|
||||
return {
|
||||
username: user.username,
|
||||
id: user.id,
|
||||
@@ -31,4 +40,70 @@ export class AuthService {
|
||||
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
@@ -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 { ConfigService } from '@nestjs/config';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class Constants {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getSecret = () => {
|
||||
return this.configService.get('JWT_SECRET');
|
||||
return this.configService.get("JWT_SECRET");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty()
|
||||
|
||||
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 { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class Profile {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiProperty()
|
||||
|
||||
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 { AuthGuard } from '@nestjs/passport';
|
||||
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 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 { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -9,7 +9,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
secretOrKey: configService.get("JWT_SECRET"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||
export class LocalAuthGuard extends AuthGuard("local") {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Strategy } from 'passport-local';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import PayloadInterface from './interface/payload.interface';
|
||||
import { Strategy } from "passport-local";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import PayloadInterface from "./interface/payload.interface";
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class JwtToken {
|
||||
@ApiProperty()
|
||||
|
||||
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 { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateGenreDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -13,20 +13,30 @@ import {
|
||||
Query,
|
||||
Req,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { Plage } from 'src/models/plage';
|
||||
import { CreateGenreDto } from './dto/create-genre.dto';
|
||||
import { Request } from 'express';
|
||||
import { GenreService } from './genre.service';
|
||||
import { Prisma, Genre } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateGenreDto } from "./dto/create-genre.dto";
|
||||
import { Request } from "express";
|
||||
import { GenreService } from "./genre.service";
|
||||
import { Prisma, Genre } from "@prisma/client";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
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')
|
||||
@ApiTags('genre')
|
||||
@Controller("genre")
|
||||
@ApiTags("genre")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
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) {}
|
||||
|
||||
@@ -39,22 +49,23 @@ export class GenreController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.service.delete({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
@Get(":id/illustration")
|
||||
@Public()
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
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`;
|
||||
if (!existsSync(path))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(path);
|
||||
@@ -65,26 +76,36 @@ export class GenreController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponsePlaginated(_Genre)
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(GenreController.filterableFields)
|
||||
where: Prisma.GenreWhereInput,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Genre>> {
|
||||
const ret = await this.service.list({
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, req, GenreController.includableFields),
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const res = await this.service.get({ id });
|
||||
@Get(":id")
|
||||
async findOne(
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { GenreController } from './genre.controller';
|
||||
import { GenreService } from './genre.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { GenreController } from "./genre.controller";
|
||||
import { GenreService } from "./genre.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Genre } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Genre } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
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({
|
||||
where,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,14 +28,16 @@ export class GenreService {
|
||||
cursor?: Prisma.GenreWhereUniqueInput;
|
||||
where?: Prisma.GenreWhereInput;
|
||||
orderBy?: Prisma.GenreOrderByWithRelationInput;
|
||||
include?: Prisma.GenreInclude;
|
||||
}): Promise<Genre[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.genre.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||