186 Commits

Author SHA1 Message Date
GitBluub
e4a8eba699 feat: gen cover in populate wip 2023-11-15 13:08:16 +01:00
bfb6cf5958 Disable jwt auth for images routes 2023-10-12 13:34:56 +02:00
a92ca75760 Fix dev nginx 2023-10-12 12:47:48 +02:00
8d8323e382 Cleanup inports 2023-10-12 12:47:48 +02:00
76d7e69d19 Add includable fields for all ressources 2023-10-12 12:47:48 +02:00
be58e932a9 Run prettier 2023-10-12 12:47:48 +02:00
38bbe56e9b Add robot tests 2023-10-12 12:47:48 +02:00
a65ce6595a Add a generic include system and implement it for songs 2023-10-12 12:47:48 +02:00
Arthur Jamet
90f7890e5f Update README (#314)
* Update README

* README: Fixes cause me dumb
2023-10-09 16:46:35 +02:00
Arthur Jamet
911e174aef Front: Update splashscreen (#312) 2023-10-08 06:56:16 +02:00
Arthur Jamet
6d7f46c425 Merge pull request #308 from Chroma-Case/front/fix-oops 2023-10-05 12:09:56 +02:00
Arthur Jamet
b72e7a54e5 Front: Fix Oops page 2023-10-05 10:25:51 +02:00
Arthur Jamet
d99d134382 Front: Fix Web Build + Improve CI (#302)
Co-authored-by: Clément Le Bihan <clement.lebihan773@gmail.com>
2023-10-03 14:56:09 +02:00
Arthur Jamet
576675411a Merge pull request #292 from Chroma-Case/front/fix-expo 2023-10-02 18:01:43 +02:00
Arthur Jamet
d214558bc4 Front: Remove unused import 2023-10-02 17:06:02 +02:00
Arthur Jamet
4299a93afe Front: Fix env var 2023-10-02 16:56:46 +02:00
Arthur Jamet
920126a392 Front: Set Env Vars 2023-10-02 14:09:17 +02:00
Arthur Jamet
16e6a5e21b Front: Fix Icon dimensions 2023-10-01 11:40:55 +02:00
Arthur Jamet
9539018b64 .env.example: add new env var 2023-10-01 11:22:06 +02:00
Arthur Jamet
0081eb2acd Front: EAS: Fix project slug 2023-10-01 11:21:20 +02:00
Arthur Jamet
bcb0825f5a Front: remove duplicate deps 2023-09-30 14:28:29 +02:00
Arthur Jamet
18a3fa518c Front: Add missing dependency 2023-09-30 14:07:14 +02:00
Arthur Jamet
0407f5c29e Front: Add missing dependency 2023-09-30 12:07:53 +02:00
Arthur Jamet
6dafe2a8e9 Front: try a custom fork 2023-09-30 11:52:10 +02:00
Arthur Jamet
4a8f0aa1af Front: Add missing eslint deps 2023-09-30 11:17:43 +02:00
Arthur Jamet
745b20358d Front: Add eslint in dev deps 2023-09-30 11:14:04 +02:00
Arthur Jamet
0f544b31f3 Front: Add prettier in dev deps 2023-09-30 11:08:56 +02:00
Arthur Jamet
76d70f3edd Front: Typecheck 2023-09-30 11:05:08 +02:00
Arthur Jamet
1c17ac8b13 Front: Fix missing dependencies 2023-09-30 10:45:23 +02:00
Arthur Jamet
232579e75b Front: Add Dependencies 2023-09-30 10:23:02 +02:00
Arthur Jamet
01221eda00 Front: Add dependencies 2023-09-29 18:34:46 +02:00
Arthur Jamet
b73c2fef58 Front: Add dependencies 2023-09-29 18:17:55 +02:00
Arthur Jamet
3c9c1b5ff7 Front: Add dependencies 2023-09-29 18:00:11 +02:00
Arthur Jamet
e50b1c1344 Front: Install Dev client 2023-09-29 16:03:50 +02:00
Arthur Jamet
6dfc531891 Front: Install Jest 2023-09-29 15:53:20 +02:00
Arthur Jamet
b4f268dee0 Front: Redump Expo 2023-09-29 15:47:16 +02:00
Arthur Jamet
e366fa4b32 Merge pull request #282 from Chroma-Case/feature/adc/retour-utilisateur
Feature/adc/retour utilisateur
2023-09-26 07:31:19 +02:00
Arthur Jamet
cd87451208 Front: Typechecking 2023-09-25 17:55:46 +02:00
danis
845c473ed5 removed useless function 2023-09-25 17:17:05 +02:00
danis
f4d75eef73 css whatever + pretty 2023-09-25 14:51:20 +02:00
danis
5395bbb03a Duration component 2023-09-25 14:24:08 +02:00
danis
2d90c6eec1 pretty 2023-09-22 15:50:26 +02:00
danis
0b0fd0585d added DurationInfo 2023-09-22 15:49:12 +02:00
danis
6cf72dfcca Merge branch 'main' into feature/adc/retour-utilisateur 2023-09-22 15:11:33 +02:00
danis
a81c0b83bb song length SongRow 2023-09-22 15:08:28 +02:00
danis
b2fb497ecf populate.py updated with midi length 2023-09-22 14:53:36 +02:00
445816dfad Fix log error for images 2023-09-21 17:03:18 +02:00
GitBluub
4a4f9e2a55 fix: email template 2023-09-21 15:23:07 +02:00
3860c9f72a Fix prettier 2023-09-21 15:11:21 +02:00
b02b23a978 Fix signup mismatch 2023-09-21 15:06:38 +02:00
5b0c1f8992 Fix verify mail 2023-09-21 14:38:51 +02:00
Bluub
8155549031 feat: back password reset email (#277) 2023-09-21 14:29:09 +02:00
Amaury
1ca4633360 Merge pull request #271 from Chroma-Case/feature/adc/#242-liked-songs
Feature/adc/#242 liked songs
2023-09-21 12:05:16 +02:00
danis
bb304fa8cd merge main into liked songs 2023-09-21 12:04:30 +02:00
Bluub
9a1f1f78cb Merge pull request #275 from Chroma-Case/logs 2023-09-21 00:49:24 +02:00
GitBluub
96bb830600 fix: try to fix scoro tests 2023-09-21 00:43:45 +02:00
Bluub
1333b74001 Merge branch 'main' into logs 2023-09-21 00:35:37 +02:00
GitBluub
ece87dbdb9 fix: try to fix scoro tests 2023-09-21 00:26:23 +02:00
GitBluub
e82a6b1dd6 fix: try to fix scoro tests 2023-09-21 00:17:02 +02:00
GitBluub
cd2e119dc6 fix: separate file for logging containers 2023-09-21 00:09:41 +02:00
Clément Le Bihan
c9928f1cce Merge pull request #276 from Chroma-Case/redesign-settings 2023-09-21 00:01:13 +02:00
Clément Le Bihan
7aac3922d6 Merge remote-tracking branch 'origin' into redesign-settings 2023-09-20 23:59:51 +02:00
GitBluub
82403c811e fix: format 2023-09-20 23:54:04 +02:00
GitBluub
230c60bcd0 fix: ok instead of created 201 -> 200 2023-09-20 23:46:38 +02:00
GitBluub
177e903b07 fix: ok instead of created 201 -> 200 2023-09-20 23:46:38 +02:00
GitBluub
a11c236753 fix: ok instead of created 201 -> 200 2023-09-20 23:46:38 +02:00
GitBluub
29ef585410 doc: genre, lesson and history controller 2023-09-20 23:46:38 +02:00
GitBluub
f8be2c2462 doc: artist and album controller 2023-09-20 23:46:38 +02:00
GitBluub
7d27af1e2d doc: search controller 2023-09-20 23:46:38 +02:00
GitBluub
258fe91ae7 doc: song controller 2023-09-20 23:46:38 +02:00
GitBluub
711b5d583b doc: users controller 2023-09-20 23:46:38 +02:00
GitBluub
4416808056 doc: auth controller 2023-09-20 23:46:38 +02:00
GitBluub
979c27c087 feat: doc for app controller 2023-09-20 23:46:38 +02:00
GitBluub
b3117886cf fix: class gen folder in gitignore 2023-09-20 23:46:38 +02:00
GitBluub
1c248fa479 fix: model for the plagination in swagger 2023-09-20 23:46:38 +02:00
GitBluub
ec62f4b085 feat: prisma class generator and models in the swagger 2023-09-20 23:46:38 +02:00
GitBluub
04bad30aaa feat: install prisma class generator 2023-09-20 23:46:38 +02:00
mathysPaul
e5a52d0f94 fix checkbox Profile off-screen 2023-09-20 18:34:20 +02:00
mathysPaul
68c6c6fa11 fixing error from CI 2023-09-20 17:59:36 +02:00
mathysPaul
94a64d16e6 Redesign profil with datafake for skills 2023-09-20 17:40:52 +02:00
7aa7f50ecb Fix prod nginx 2023-09-20 14:58:09 +02:00
ee8e0e26db Fix eslint and bad reverify mail issue 2023-09-20 14:58:09 +02:00
31b965e8f6 Add volume/enable state and follow the music's bpm for the metronome 2023-09-20 14:58:09 +02:00
94658d4379 Add static assets to nginx 2023-09-20 14:58:09 +02:00
Clément Le Bihan
49a735631a prettied 2023-09-20 13:39:26 +02:00
Clément Le Bihan
1905daec60 MainHomeCard is now displaying the first 4 songs 2023-09-20 13:39:26 +02:00
Clément Le Bihan
7a1f4fb787 Fix to really allow guest accounts empty strings are transformed to null values 2023-09-20 13:39:26 +02:00
Clément Le Bihan
f3cdba34fb Now usign real play history for the TabNavigator Desktop 2023-09-20 13:39:26 +02:00
Clément Le Bihan
5b7cb6746d Added specific fontSizes for each card 2023-09-20 13:39:26 +02:00
Clément Le Bihan
6e3e73982f Added callback for onPress for the SongCardInfos and replaced the button to have the play icon more centered but some state issue 2023-09-20 13:39:26 +02:00
Clément Le Bihan
8e5c65e6f2 Added SongCardInfo for the V2 design and type fixes 2023-09-20 13:39:26 +02:00
Clément Le Bihan
94875d4c7f trying golden ratio 2023-09-20 13:39:26 +02:00
Clément Le Bihan
e817021ede fix type errors 2023-09-20 13:39:26 +02:00
Clément Le Bihan
dcca1b1f1c Added phone and responsive support on the tabnavigation added callapsables fixed colorscheme and setting background color 2023-09-20 13:39:26 +02:00
Clément Le Bihan
c0c2918e72 Started navigation 2023-09-20 13:39:26 +02:00
mathysPaul
973f9bf5b3 redesign AuthenticationView 2023-09-20 10:27:24 +02:00
GitBluub
162fc9148f grafana: auto setup of dashboard 2023-09-20 00:26:15 +02:00
GitBluub
57d646f6eb scoro: direct log to loki not working 2023-09-20 00:25:26 +02:00
mathysPaul
6768b0b2a6 merge main 2023-09-19 19:22:25 +02:00
mathysPaul
fa14d1f979 Fixing error prettier redesign CI 2023-09-19 18:43:38 +02:00
mathysPaul
c4ca2e509e Fixing redesign-settings prettier & lint => CI 2023-09-19 18:23:31 +02:00
mathysPaul
1abfbf391f Fixing error prettier redesign CI 2023-09-19 17:36:19 +02:00
mathysPaul
073ff033f3 Fixing error redesign CI 2023-09-19 17:12:49 +02:00
GitBluub
23e5941700 scoro: log directly to loki 2023-09-19 17:11:42 +02:00
Clément Le Bihan
027d450579 Forgot a merge conflict 2023-09-19 15:19:42 +02:00
Clément Le Bihan
ad9bbbc2b9 Cleanup random 2023-09-19 15:19:42 +02:00
Clément Le Bihan
58af78b1d3 prettied phaserCanvas.ts 2023-09-19 15:19:42 +02:00
Clément Le Bihan
09d2da8eec Fixed scaling issue with the cursor position texture size is still a concern 2023-09-19 15:19:42 +02:00
Clément Le Bihan
8abaaf6624 style the scaling not working to fix 2023-09-19 15:19:42 +02:00
Clément Le Bihan
3c3697be61 fix test back for duplicated user 2023-09-19 15:19:42 +02:00
Clément Le Bihan
073c00a35e Fixed a bug when current streak is 0 and Linter fix 2023-09-19 15:19:42 +02:00
Clément Le Bihan
58d761c359 prettier cleanup 2023-09-19 15:19:42 +02:00
Clément Le Bihan
aaaf73f632 PR cleanup 2023-09-19 15:19:42 +02:00
Clément Le Bihan
f83043a9c9 Handling in satisfactory manner scoro messages 2023-09-19 15:19:42 +02:00
Clément Le Bihan
cea6d8d0bc Added the message pinao system reusing a react context for simplicity and emitting note timing messages when scoro gives the result 2023-09-19 15:19:42 +02:00
Clément Le Bihan
607c35b621 Added first effect of particules 2023-09-19 15:19:42 +02:00
Clément Le Bihan
13d0be4586 Small QoL fixes thare were really needed 2023-09-19 15:19:42 +02:00
danis
3e1e41f117 pretty 2023-09-19 09:39:54 +02:00
danis
8f9d7e4a85 typo 2023-09-19 09:37:00 +02:00
mathysPaul
1e504c8982 Redesign settings 2023-09-19 03:54:12 +02:00
danis
e56436db3a merging main into feature/adc/#242-liked-songs 2023-09-18 16:49:47 +02:00
danis
bc227fb0ea pretty + better handling + handling in artist detail view 2023-09-18 16:45:03 +02:00
Clément Le Bihan
49bc4f9f45 Update front/views/StartPageView.tsx 2023-09-18 15:37:58 +02:00
Arthur Jamet
73076c4b28 Front: Recover package.json 2023-09-18 15:37:58 +02:00
Arthur Jamet
8732972b3f Front: Recover yarn.lock 2023-09-18 15:37:58 +02:00
Arthur Jamet
cd9d64e501 Front: Prettier 2023-09-18 15:37:58 +02:00
Arthur Jamet
62bf7ec035 Front: Apply New Color, Button and Link Style 2023-09-18 15:37:58 +02:00
Arthur Jamet
659f5d5d84 Front: Setup New Font 2023-09-18 15:37:58 +02:00
Arthur Jamet
bbc53f04de Front: Get Rid of external image, load local assets 2023-09-18 15:37:58 +02:00
danis
431427d7ad fixed mirgation + back-end + front end filter, heart shaped button and special FavSongRow 2023-09-17 20:57:10 +02:00
GitBluub
611ab57c5d scoro: game uuid for logging and bug fixing 2023-09-16 16:55:55 +02:00
bc13c10f1a Fix ci 2023-09-15 17:57:03 +02:00
91c9e2b295 Update .env.example to use dummy values for the ci 2023-09-15 17:57:03 +02:00
585be2aa19 Fix prettier warnings 2023-09-15 17:57:03 +02:00
654022b48a Update .env.example 2023-09-15 17:57:03 +02:00
afab03baf8 Add a button to resend verified mail 2023-09-15 17:57:03 +02:00
a52c10fc2c Add verified badge and page on the front 2023-09-15 17:57:03 +02:00
f2ed598865 Use a fixed python version for the scorometer 2023-09-15 17:57:03 +02:00
02fc8175f4 Send mails on account creation 2023-09-15 17:57:03 +02:00
Arthur Jamet
628e50a48d Merge pull request #257 from Chroma-Case/feature/adc/#224-genre-view
Feature/adc/#224 genre view
2023-09-14 15:33:40 +02:00
Arthur Jamet
70ab56ce3a Front: Remove unused value 2023-09-14 11:41:38 +02:00
Arthur Jamet
1fefe7912d Front: Run Pretty 2023-09-14 11:37:50 +02:00
danis
c21f5f0659 Merge branch 'feature/adc/#224-genre-view' into feature/adc/#242-liked-songs 2023-09-13 13:23:16 +02:00
danis
46ef0a7f1b remove expo-linear-gradient 2023-09-12 22:05:31 +02:00
danis
b43c64962a favorites search view filter + song query from favorites data 2023-09-10 14:48:39 +02:00
danis
64640eda55 lints fix 2023-09-09 19:18:30 +02:00
danis
a6d9cb3b40 run prettier 2023-09-09 18:55:32 +02:00
danis
b61541f7b8 fix PR III 2023-09-09 17:52:22 +02:00
danis
3ff523560b fix PR II 2023-09-09 17:51:18 +02:00
danis
b61968706d fix PR I 2023-09-09 14:25:43 +02:00
Arthur Jamet
2f27278d3a Front: Pretty 2023-09-08 17:53:23 +02:00
Arthur Jamet
e1ab9fe118 Front: Fix an error that occured on prod, caused by the avatar's url 2023-09-08 17:53:23 +02:00
Arthur Jamet
b1d0415ba0 Front: Fix genre view 2023-09-07 17:10:18 +02:00
Arthur Jamet
8ab85ab689 Front: remove file64 dependency 2023-09-07 17:06:27 +02:00
danis
16cd794e3b trial for artist name 2023-09-07 10:31:03 +02:00
danis
f85c30a53b clean code VI 2023-09-06 17:07:16 +02:00
danis
6da96ed886 clean code V 2023-09-06 17:00:36 +02:00
danis
852fbd5c87 clean code IV 2023-09-06 16:39:38 +02:00
danis
5cec62d1b1 search view update 2023-09-06 16:38:44 +02:00
danis
7e866f9826 clean code III 2023-09-06 15:59:50 +02:00
danis
2f50f694f3 clean code 2023-09-06 15:57:38 +02:00
danis
c9d3ef88e7 clean code + search history handler fix 2023-09-05 13:44:30 +02:00
danis
0ba3bec5aa Merge branch 'main' into feature/adc/#224-genre-view 2023-09-05 09:41:20 +02:00
danis
539c35c903 song cards routing fix 2023-09-05 09:36:11 +02:00
danis
e1463d41b9 actual data from db tho needs better design care 2023-09-05 09:33:31 +02:00
danis
01394056a6 Merge branch 'feature/adc/artist-view' into feature/adc/#224-genre-view 2023-09-04 14:24:21 +02:00
danis
1396fcb39c artist name fix 2023-09-04 11:05:33 +02:00
danis
c81f8df61c prisma migration + back auth/me/likes + front API add and get methods for liked song 2023-08-30 13:06:25 +02:00
danis
1255343b97 artist view + moved components 2023-08-12 11:16:22 +02:00
danis
f7562c18bd basic genre details view 2023-08-12 10:43:02 +02:00
GitBluub
dc398d6e06 rm useless file 2023-07-26 22:22:03 +09:00
GitBluub
d5da112a01 scorometer create uuid 2023-07-26 22:21:36 +09:00
GitBluub
96048bd671 back logging every request 2023-07-26 22:21:21 +09:00
GitBluub
dcdc6b196d grafana setup and dashboard json 2023-07-26 22:21:00 +09:00
GitBluub
2ec95dd3c3 wip 2023-07-23 18:14:11 +09:00
danis
bf09a25eb5 linear gradient 2023-07-11 10:06:55 +02:00
danis
373128ba53 broke my glasses 2023-07-10 23:12:37 +02:00
danis
3a09d10d3b you miss 100% of the shots you dont take 2023-07-09 23:24:31 +02:00
Arthur Jamet
87de52cae0 Front: 'Get Song By Artist' Query: fix typings 2023-07-05 14:18:31 +01:00
Arthur Jamet
931fe13eee Merge branch 'main' of github.com:Chroma-Case/Chromacase into feature/adc/artist-view 2023-07-05 14:06:27 +01:00
danis
28716eeab2 init genreDetailsView 2023-07-05 09:26:45 +02:00
GitBluub
5a190f3b96 wip 2023-06-28 22:03:59 +09:00
danis
606af3901c Merge branch 'main' into feature/adc/artist-view 2023-06-28 09:22:25 +02:00
danis
b2247e79ae having a bug with api :/ 2023-06-28 09:11:49 +02:00
danis
a6ae770194 Merge branch 'main' into feature/adc/artist-view 2023-06-21 09:19:04 +02:00
danis
e378465126 components RowCustom & SongRow + artist banner 2023-06-21 08:21:34 +02:00
219 changed files with 22628 additions and 14299 deletions

View File

@@ -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

View File

@@ -7,6 +7,12 @@ 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

5
.envrc
View File

@@ -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

View File

@@ -27,6 +27,25 @@ jobs:
## Build App ##
Check_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
Build_Front:
runs-on: ubuntu-latest
defaults:
@@ -43,19 +62,21 @@ jobs:
- 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 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: |
@@ -93,7 +114,7 @@ jobs:
run: |
docker-compose ps -a
docker-compose logs
wget --retry-connrefused http://localhost:3000 # /healthcheck
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
- name: Run scorometer tests
run: |

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@ log.html
node_modules/
./front/coverage
.venv
.data
.DS_Store
_gen

View File

@@ -1,9 +1,39 @@
# ![Chromacase](./assets/graphical/title.png)
# ![Chromacase](./assets/graphical/banner.png)
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
![Schéma Fonctionnel](./assets/docs/structure.png)
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) |

BIN
assets/graphical/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

View File

@@ -1,9 +1,10 @@
#!/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")
@@ -20,16 +21,16 @@ def getOrCreateAlbum(name, artistId):
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,
})
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={
@@ -39,11 +40,19 @@ def getOrCreateArtist(name):
print(out)
return out["id"]
def gen_cover():
def populateFile(path, midi, mxl):
config = ConfigParser()
config.read(path)
mid = MidiFile(midi)
common = os.path.commonpath([midi, mxl])
png_path = f"{common}/illustration.png"
if not os.path.exists(png_path):
gen_cover(common)
metadata = config["Metadata"];
difficulties = dict(config["Difficulties"])
difficulties["length"] = round((mid.length), 2)
artistId = getOrCreateArtist(metadata["Artist"])
print(f"Populating {metadata['Name']}")
res = requests.post(f"{url}/song", json={
@@ -54,11 +63,10 @@ def populateFile(path, midi, mxl):
"artist": artistId,
"album": getOrCreateAlbum(metadata["Album"], artistId),
"genre": getOrCreateGenre(metadata["Genre"]),
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
"illustrationPath": f"/assets/{png_path}"
})
print(res.json())
def main():
global url
if url == None:

View File

@@ -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

10208
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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,6 +21,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/common": "^10.1.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.1.0",
@@ -35,11 +36,15 @@
"@types/passport": "^1.0.12",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"json-logger-service": "^9.0.1",
"class-validator": "^0.14.0",
"node-fetch": "^2.6.12",
"nodemailer": "^6.9.5",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"prisma-class-generator": "^0.2.7",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.1",
"rxjs": "^7.8.1",
@@ -53,6 +58,7 @@
"@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",

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -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;

View 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");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL;

View File

@@ -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")
@@ -13,7 +19,8 @@ model User {
id Int @id @default(autoincrement())
username String @unique
password String?
email String
email String? @unique
emailVerified Boolean @default(false)
googleID String? @unique
isGuest Boolean @default(false)
partyPlayed Int @default(0)
@@ -21,6 +28,16 @@ model User {
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 {
@@ -60,6 +77,7 @@ model Song {
genre Genre? @relation(fields: [genreId], references: [id])
difficulties Json
SongHistory SongHistory[]
likedByUsers LikedSongs[]
}
model SongHistory {

View File

@@ -1,5 +1,4 @@
import {
BadRequestException,
Body,
ConflictException,
Controller,
@@ -12,23 +11,35 @@ import {
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { Plage } from 'src/models/plage';
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 { ApiTags } from '@nestjs/swagger';
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 { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
@Controller('album')
@ApiTags('album')
@UseGuards(JwtAuthGuard)
export class AlbumController {
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({
@@ -45,6 +56,7 @@ export class AlbumController {
}
@Delete(':id')
@ApiOperation({ description: 'Delete an album by id' })
async remove(@Param('id', ParseIntPipe) id: number) {
try {
return await this.albumService.deleteAlbum({ id });
@@ -54,10 +66,13 @@ export class AlbumController {
}
@Get()
@ApiOkResponsePlaginated(_Album)
@ApiOperation({ description: 'Get all albums paginated' })
async findAll(
@Req() req: Request,
@FilterQuery(AlbumController.filterableFields)
where: Prisma.AlbumWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Album>> {
@@ -65,13 +80,23 @@ export class AlbumController {
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 });
@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');
return res;

View File

@@ -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,
});
}

View File

@@ -1,11 +1,15 @@
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();
}

View File

@@ -14,6 +14,7 @@ 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';
@Module({
imports: [
@@ -28,6 +29,12 @@ import { HistoryModule } from './history/history.module';
SearchModule,
SettingsModule,
HistoryModule,
MailerModule.forRoot({
transport: process.env.SMTP_TRANSPORT,
defaults: {
from: process.env.MAIL_AUTHOR,
},
}),
],
controllers: [AppController],
providers: [AppService, PrismaService, ArtistService],

View File

@@ -1,5 +1,4 @@
import {
BadRequestException,
Body,
ConflictException,
Controller,
@@ -14,24 +13,42 @@ import {
Query,
Req,
StreamableFile,
UseGuards,
} from '@nestjs/common';
import { Plage } from 'src/models/plage';
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 { ApiTags } from '@nestjs/swagger';
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 { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { Public } from 'src/auth/public';
@Controller('artist')
@ApiTags('artist')
@UseGuards(JwtAuthGuard)
export class ArtistController {
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);
@@ -41,6 +58,7 @@ export class ArtistController {
}
@Delete(':id')
@ApiOperation({ description: 'Delete an artist by id' })
async remove(@Param('id', ParseIntPipe) id: number) {
try {
return await this.service.delete({ id });
@@ -50,6 +68,9 @@ export class ArtistController {
}
@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');
@@ -66,10 +87,13 @@ export class ArtistController {
}
@Get()
@ApiOperation({ description: 'Get all artists paginated' })
@ApiOkResponsePlaginated(_Artist)
async findAll(
@Req() req: Request,
@FilterQuery(ArtistController.filterableFields)
where: Prisma.ArtistWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Artist>> {
@@ -77,13 +101,23 @@ export class ArtistController {
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 });
@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');
return res;

View File

@@ -12,9 +12,13 @@ export class ArtistService {
});
}
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,14 +28,16 @@ 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,
});
}

View File

@@ -7,6 +7,7 @@ import {
Body,
Delete,
BadRequestException,
ConflictException,
HttpCode,
Put,
InternalServerErrorException,
@@ -18,6 +19,8 @@ import {
HttpStatus,
ParseFilePipeBuilder,
Response,
Query,
Param,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -25,9 +28,12 @@ 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';
@@ -41,6 +47,7 @@ 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 ';
@ApiTags('auth')
@Controller('auth')
@@ -53,9 +60,14 @@ export class AuthController {
@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 });
@@ -67,26 +79,86 @@ export class AuthController {
}
@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);
await this.settingsService.createUserSetting(user.id);
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);
}
@HttpCode(200)
@Post('guest')
@HttpCode(200)
@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);
@@ -95,6 +167,7 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ description: 'Get the profile picture of connected user' })
@ApiOkResponse({ description: 'The user profile picture' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me/picture')
@@ -107,6 +180,7 @@ export class AuthController {
@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,
@@ -121,7 +195,7 @@ export class AuthController {
)
file: Express.Multer.File,
) {
const path = `/data/${req.user.id}.jpg`
const path = `/data/${req.user.id}.jpg`;
writeFile(path, file.buffer, (err) => {
if (err) throw err;
});
@@ -132,6 +206,7 @@ export class AuthController {
@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();
@@ -143,6 +218,7 @@ export class AuthController {
@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>,
@@ -168,6 +244,7 @@ export class AuthController {
@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 });
}
@@ -177,6 +254,7 @@ export class AuthController {
@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,
@@ -192,6 +270,7 @@ export class AuthController {
@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,
@@ -199,4 +278,31 @@ export class AuthController {
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') 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') 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) {
return this.usersService.getLikedSongs(+req.user.id);
}
}

View File

@@ -21,7 +21,7 @@ import { GoogleStrategy } from './google.strategy';
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
signOptions: { expiresIn: '365d' },
}),
inject: [ConfigService],
}),

View File

@@ -1,13 +1,16 @@
import { BadRequestException, Injectable } from '@nestjs/common';
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,
) {}
async validateUser(
@@ -31,4 +34,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;
}
}

View File

@@ -0,0 +1,8 @@
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class PasswordResetDto {
@ApiProperty()
@IsNotEmpty()
password: string;
}

View File

@@ -1,5 +1,24 @@
import { Injectable } from '@nestjs/common';
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) {
console.log(context);
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
console.log(isPublic);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

4
back/src/auth/public.ts Normal file
View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@@ -13,8 +13,9 @@ import {
Query,
Req,
StreamableFile,
UseGuards,
} from '@nestjs/common';
import { Plage } from 'src/models/plage';
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateGenreDto } from './dto/create-genre.dto';
import { Request } from 'express';
import { GenreService } from './genre.service';
@@ -22,11 +23,19 @@ 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 { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { Public } from 'src/auth/public';
@Controller('genre')
@ApiTags('genre')
@UseGuards(JwtAuthGuard)
export class GenreController {
static filterableFields: string[] = ['+id', 'name'];
static includableFields: IncludeMap<Prisma.GenreInclude> = {
Song: true,
};
constructor(private readonly service: GenreService) {}
@@ -49,6 +58,7 @@ export class GenreController {
}
@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');
@@ -65,10 +75,12 @@ export class GenreController {
}
@Get()
@ApiOkResponsePlaginated(_Genre)
async findAll(
@Req() req: Request,
@FilterQuery(GenreController.filterableFields)
where: Prisma.GenreWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Genre>> {
@@ -76,13 +88,21 @@ export class GenreController {
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 });
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');
return res;

View File

@@ -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,
});
}

View File

@@ -1,9 +1,9 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty } from '@nestjs/swagger';
export class SearchHistoryDto {
@ApiProperty()
query: string;
@ApiProperty()
type: "song" | "artist" | "album" | "genre";
type: 'song' | 'artist' | 'album' | 'genre';
}

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNumber } from "class-validator";
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class SongHistoryDto {
@ApiProperty()
@@ -15,8 +15,8 @@ export class SongHistoryDto {
score: number;
@ApiProperty()
difficulties: Record<string, number>
difficulties: Record<string, number>;
@ApiProperty()
info: Record<string, number>
info: Record<string, number>;
}

View File

@@ -10,21 +10,31 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import {
ApiCreatedResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { SearchHistory, SongHistory } from '@prisma/client';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { SongHistoryDto } from './dto/SongHistoryDto';
import { HistoryService } from './history.service';
import { SearchHistoryDto } from './dto/SearchHistoryDto';
import { SongHistory as _SongHistory } from 'src/_gen/prisma-class/song_history';
import { SearchHistory as _SearchHistory } from 'src/_gen/prisma-class/search_history';
@Controller('history')
@ApiTags('history')
export class HistoryController {
constructor(private readonly historyService: HistoryService) { }
constructor(private readonly historyService: HistoryService) {}
@Get()
@HttpCode(200)
@ApiOperation({ description: 'Get song history of connected user' })
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _SongHistory, isArray: true })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async getHistory(
@Request() req: any,
@@ -36,7 +46,9 @@ export class HistoryController {
@Get('search')
@HttpCode(200)
@ApiOperation({ description: 'Get search history of connected user' })
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _SearchHistory, isArray: true })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async getSearchHistory(
@Request() req: any,
@@ -48,18 +60,24 @@ export class HistoryController {
@Post()
@HttpCode(201)
@ApiOperation({ description: 'Create a record of a song played by a user' })
@ApiCreatedResponse({ description: 'Succesfully created a record' })
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
return this.historyService.createSongHistoryRecord(record);
}
@Post("search")
@Post('search')
@HttpCode(201)
@ApiOperation({ description: 'Creates a search record in the users history' })
@UseGuards(JwtAuthGuard)
@ApiUnauthorizedResponse({description: "Invalid token"})
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async createSearchHistory(
@Request() req: any,
@Body() record: SearchHistoryDto
): Promise<void> {
await this.historyService.createSearchHistoryRecord(req.user.id, { query: record.query, type: record.type });
}
@Body() record: SearchHistoryDto,
): Promise<void> {
await this.historyService.createSearchHistoryRecord(req.user.id, {
query: record.query,
type: record.type,
});
}
}

View File

@@ -2,17 +2,17 @@ import { Test, TestingModule } from '@nestjs/testing';
import { HistoryService } from './history.service';
describe('HistoryService', () => {
let service: HistoryService;
let service: HistoryService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HistoryService],
}).compile();
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HistoryService],
}).compile();
service = module.get<HistoryService>(HistoryService);
});
service = module.get<HistoryService>(HistoryService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -6,7 +6,7 @@ import { SongHistoryDto } from './dto/SongHistoryDto';
@Injectable()
export class HistoryService {
constructor(private prisma: PrismaService) { }
constructor(private prisma: PrismaService) {}
async createSongHistoryRecord({
songID,
@@ -74,7 +74,7 @@ export class HistoryService {
async createSearchHistoryRecord(
userID: number,
{ query, type }: SearchHistoryDto
{ query, type }: SearchHistoryDto,
): Promise<SearchHistory> {
return this.prisma.searchHistory.create({
data: {

View File

@@ -3,7 +3,6 @@ import {
Get,
Query,
Req,
Request,
Param,
ParseIntPipe,
DefaultValuePipe,
@@ -12,12 +11,17 @@ import {
Body,
Delete,
NotFoundException,
UseGuards,
} from '@nestjs/common';
import { Plage } from 'src/models/plage';
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { LessonService } from './lesson.service';
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import { Prisma, Skill } from '@prisma/client';
import { FilterQuery } from 'src/utils/filter.pipe';
import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { IncludeMap, mapInclude } from 'src/utils/include';
import { Request } from 'express';
export class Lesson {
@ApiProperty()
@@ -34,6 +38,7 @@ export class Lesson {
@ApiTags('lessons')
@Controller('lesson')
@UseGuards(JwtAuthGuard)
export class LessonController {
static filterableFields: string[] = [
'+id',
@@ -41,6 +46,9 @@ export class LessonController {
'+requiredLevel',
'mainSkill',
];
static includableFields: IncludeMap<Prisma.LessonInclude> = {
LessonHistory: true,
};
constructor(private lessonService: LessonService) {}
@@ -48,10 +56,12 @@ export class LessonController {
summary: 'Get all lessons',
})
@Get()
@ApiOkResponsePlaginated(_Lesson)
async getAll(
@Req() request: Request,
@FilterQuery(LessonController.filterableFields)
where: Prisma.LessonWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Lesson>> {
@@ -59,6 +69,7 @@ export class LessonController {
skip,
take,
where,
include: mapInclude(include, request, LessonController.includableFields),
});
return new Plage(ret, request);
}
@@ -67,8 +78,15 @@ export class LessonController {
summary: 'Get a particular lessons',
})
@Get(':id')
async get(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
const ret = await this.lessonService.get(id);
async get(
@Req() req: Request,
@Query('include') include: string,
@Param('id', ParseIntPipe) id: number,
): Promise<Lesson> {
const ret = await this.lessonService.get(
id,
mapInclude(include, req, LessonController.includableFields),
);
if (!ret) throw new NotFoundException();
return ret;
}

View File

@@ -12,22 +12,28 @@ export class LessonService {
cursor?: Prisma.LessonWhereUniqueInput;
where?: Prisma.LessonWhereInput;
orderBy?: Prisma.LessonOrderByWithRelationInput;
include?: Prisma.LessonInclude;
}): Promise<Lesson[]> {
const { skip, take, cursor, where, orderBy } = params;
const { skip, take, cursor, where, orderBy, include } = params;
return this.prisma.lesson.findMany({
skip,
take,
cursor,
where,
orderBy,
include,
});
}
async get(id: number): Promise<Lesson | null> {
async get(
id: number,
include?: Prisma.LessonInclude,
): Promise<Lesson | null> {
return this.prisma.lesson.findFirst({
where: {
id: id,
},
include,
});
}

View File

@@ -1,10 +1,56 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
ValidationPipe,
} from '@nestjs/common';
import { RequestLogger, RequestLoggerOptions } from 'json-logger-service';
import { tap } from 'rxjs';
import { PrismaModel } from './_gen/prisma-class';
import { PrismaService } from './prisma/prisma.service';
@Injectable()
export class AspectLogger implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
const { statusCode } = context.switchToHttp().getResponse();
const { originalUrl, method, params, query, body, user } = req;
const toPrint = {
originalUrl,
method,
params,
query,
body,
userId: user?.id ?? 'not logged in',
username: user?.username ?? 'not logged in',
};
return next.handle().pipe(
tap((/* data */) =>
console.log(
JSON.stringify({
...toPrint,
statusCode,
//data, //TODO: Data crashed with images
}),
),),
);
}
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
RequestLogger.buildExpressRequestLogger({
doNotLogPaths: ['/health'],
} as RequestLoggerOptions),
);
app.enableShutdownHooks();
const config = new DocumentBuilder()
@@ -12,11 +58,15 @@ async function bootstrap() {
.setDescription('The chromacase API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
const document = SwaggerModule.createDocument(app, config, {
extraModels: [...PrismaModel.extraModels],
});
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(new ValidationPipe());
app.enableCors();
app.useGlobalInterceptors(new AspectLogger());
await app.listen(3000);
}
bootstrap();

View File

@@ -2,16 +2,35 @@
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
*/
import { ApiProperty } from '@nestjs/swagger';
import { Type, applyDecorators } from '@nestjs/common';
import {
ApiExtraModels,
ApiOkResponse,
ApiProperty,
getSchemaPath,
} from '@nestjs/swagger';
export class Plage<T> {
export class PlageMetadata {
@ApiProperty()
metadata: {
this: string;
next: string | null;
previous: string | null;
};
this: string;
@ApiProperty({
type: 'string',
nullable: true,
description: "null if there is no next page, couldn't set it in swagger",
})
next: string | null;
@ApiProperty({
type: 'string',
nullable: true,
description:
"null if there is no previous page, couldn't set it in swagger",
})
previous: string | null;
}
export class Plage<T extends object> {
@ApiProperty()
metadata: PlageMetadata;
data: T[];
constructor(data: T[], request: Request | any) {
@@ -49,3 +68,25 @@ export class Plage<T> {
return route;
}
}
export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
dataDto: DataDto,
) =>
applyDecorators(
ApiExtraModels(Plage, dataDto),
ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(Plage) },
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(dataDto) },
},
},
},
],
},
}),
);

View File

@@ -6,7 +6,7 @@ export class User {
@ApiProperty()
username: string;
@ApiProperty()
email: string;
email: string | null;
@ApiProperty()
isGuest: boolean;
@ApiProperty()

View File

@@ -1,33 +1,51 @@
import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
InternalServerErrorException,
NotFoundException,
Param,
ParseIntPipe,
Post,
Query,
Request,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import {
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { Artist, Genre, Song } from '@prisma/client';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { SearchSongDto } from './dto/search-song.dto';
import { SearchService } from './search.service';
import { Song as _Song } from 'src/_gen/prisma-class/song';
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
import { mapInclude } from 'src/utils/include';
import { SongController } from 'src/song/song.controller';
import { GenreController } from 'src/genre/genre.controller';
import { ArtistController } from 'src/artist/artist.controller';
@ApiTags('search')
@Controller('search')
export class SearchController {
constructor(private readonly searchService: SearchService) { }
constructor(private readonly searchService: SearchService) {}
@Get('songs/:query')
@ApiOkResponse({ type: _Song, isArray: true })
@ApiOperation({ description: 'Search a song' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@UseGuards(JwtAuthGuard)
async searchSong(@Request() req: any, @Param('query') query: string): Promise<Song[] | null> {
async searchSong(
@Request() req: any,
@Query('include') include: string,
@Param('query') query: string,
): Promise<Song[] | null> {
try {
const ret = await this.searchService.songByGuess(query, req.user?.id);
const ret = await this.searchService.songByGuess(
query,
req.user?.id,
mapInclude(include, req, SongController.includableFields),
);
if (!ret.length) throw new NotFoundException();
else return ret;
} catch (error) {
@@ -37,9 +55,20 @@ export class SearchController {
@Get('genres/:query')
@UseGuards(JwtAuthGuard)
async searchGenre(@Request() req: any, @Param('query') query: string): Promise<Genre[] | null> {
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@ApiOkResponse({ type: _Genre, isArray: true })
@ApiOperation({ description: 'Search a genre' })
async searchGenre(
@Request() req: any,
@Query('include') include: string,
@Param('query') query: string,
): Promise<Genre[] | null> {
try {
const ret = await this.searchService.genreByGuess(query, req.user?.id);
const ret = await this.searchService.genreByGuess(
query,
req.user?.id,
mapInclude(include, req, GenreController.includableFields),
);
if (!ret.length) throw new NotFoundException();
else return ret;
} catch (error) {
@@ -49,13 +78,24 @@ export class SearchController {
@Get('artists/:query')
@UseGuards(JwtAuthGuard)
async searchArtists(@Request() req: any, @Param('query') query: string): Promise<Artist[] | null> {
@ApiOkResponse({ type: _Artist, isArray: true })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@ApiOperation({ description: 'Search an artist' })
async searchArtists(
@Request() req: any,
@Query('include') include: string,
@Param('query') query: string,
): Promise<Artist[] | null> {
try {
const ret = await this.searchService.artistByGuess(query, req.user?.id);
const ret = await this.searchService.artistByGuess(
query,
req.user?.id,
mapInclude(include, req, ArtistController.includableFields),
);
if (!ret.length) throw new NotFoundException();
else return ret;
} catch (error) {
throw new InternalServerErrorException();
}
}
}
}

View File

@@ -1,33 +1,51 @@
import { Injectable } from '@nestjs/common';
import { Album, Artist, Prisma, Song, Genre } from '@prisma/client';
import { Artist, Prisma, Song, Genre } from '@prisma/client';
import { HistoryService } from 'src/history/history.service';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class SearchService {
constructor(private prisma: PrismaService, private history: HistoryService) { }
constructor(
private prisma: PrismaService,
private history: HistoryService,
) {}
async songByGuess(query: string, userID: number): Promise<Song[]> {
async songByGuess(
query: string,
userID: number,
include?: Prisma.SongInclude,
): Promise<Song[]> {
return this.prisma.song.findMany({
where: {
name: { contains: query, mode: 'insensitive' },
},
include,
});
}
async genreByGuess(query: string, userID: number): Promise<Genre[]> {
async genreByGuess(
query: string,
userID: number,
include?: Prisma.GenreInclude,
): Promise<Genre[]> {
return this.prisma.genre.findMany({
where: {
name: { contains: query, mode: 'insensitive' },
},
include,
});
}
async artistByGuess(query: string, userID: number): Promise<Artist[]> {
async artistByGuess(
query: string,
userID: number,
include?: Prisma.ArtistInclude,
): Promise<Artist[]> {
return this.prisma.artist.findMany({
where: {
name: { contains: query, mode: 'insensitive' },
},
include,
});
}
}

View File

@@ -3,8 +3,8 @@ import { SettingsService } from './settings.service';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [SettingsService],
exports: [SettingsService],
imports: [PrismaModule],
providers: [SettingsService],
exports: [SettingsService],
})
export class SettingsModule {}

View File

@@ -20,10 +20,10 @@ export class SettingsService {
user: {
connect: {
id: userId,
}
}
}
})
},
},
},
});
}
async updateUserSettings(params: {
@@ -37,7 +37,9 @@ export class SettingsService {
});
}
async deleteUserSettings(where: Prisma.UserSettingsWhereUniqueInput): Promise<UserSettings> {
async deleteUserSettings(
where: Prisma.UserSettingsWhereUniqueInput,
): Promise<UserSettings> {
return this.prisma.userSettings.delete({
where,
});

View File

@@ -16,19 +16,38 @@ import {
StreamableFile,
UseGuards,
} from '@nestjs/common';
import { Plage } from 'src/models/plage';
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateSongDto } from './dto/create-song.dto';
import { SongService } from './song.service';
import { Request } from 'express';
import { Prisma, Song } from '@prisma/client';
import { createReadStream, existsSync } from 'fs';
import { ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import {
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiProperty,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { HistoryService } from 'src/history/history.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { FilterQuery } from 'src/utils/filter.pipe';
import { Song as _Song } from 'src/_gen/prisma-class/song';
import { SongHistory } from 'src/_gen/prisma-class/song_history';
import { IncludeMap, mapInclude } from 'src/utils/include';
import { Public } from 'src/auth/public';
class SongHistoryResult {
@ApiProperty()
best: number;
@ApiProperty({ type: SongHistory, isArray: true })
history: SongHistory[];
}
@Controller('song')
@ApiTags('song')
@UseGuards(JwtAuthGuard)
export class SongController {
static filterableFields: string[] = [
'+id',
@@ -37,6 +56,13 @@ export class SongController {
'+albumId',
'+genreId',
];
static includableFields: IncludeMap<Prisma.SongInclude> = {
artist: true,
album: true,
genre: true,
SongHistory: ({ user }) => ({ where: { userID: user.id } }),
likedByUsers: ({ user }) => ({ where: { userId: user.id } }),
};
constructor(
private readonly songService: SongService,
@@ -44,6 +70,9 @@ export class SongController {
) {}
@Get(':id/midi')
@ApiOperation({ description: 'Streams the midi file of the requested song' })
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: 'Returns the midi file succesfully' })
async getMidi(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException('Song not found');
@@ -57,6 +86,12 @@ export class SongController {
}
@Get(':id/illustration')
@ApiOperation({
description: 'Streams the illustration of the requested song',
})
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
@Public()
async getIllustration(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException('Song not found');
@@ -74,6 +109,11 @@ export class SongController {
}
@Get(':id/musicXml')
@ApiOperation({
description: 'Streams the musicXML file of the requested song',
})
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
async getMusicXml(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException('Song not found');
@@ -83,6 +123,10 @@ export class SongController {
}
@Post()
@ApiOperation({
description:
'register a new song in the database, should not be used by the frontend',
})
async create(@Body() createSongDto: CreateSongDto) {
try {
return await this.songService.createSong({
@@ -105,6 +149,7 @@ export class SongController {
}
@Delete(':id')
@ApiOperation({ description: 'delete a song by id' })
async remove(@Param('id', ParseIntPipe) id: number) {
try {
return await this.songService.deleteSong({ id });
@@ -114,9 +159,11 @@ export class SongController {
}
@Get()
@ApiOkResponsePlaginated(_Song)
async findAll(
@Req() req: Request,
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Song>> {
@@ -124,13 +171,26 @@ export class SongController {
skip,
take,
where,
include: mapInclude(include, req, SongController.includableFields),
});
return new Plage(ret, req);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.songService.song({ id });
@ApiOperation({ description: 'Get a specific song data' })
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ type: _Song, description: 'Requested song' })
async findOne(
@Req() req: Request,
@Param('id', ParseIntPipe) id: number,
@Query('include') include: string,
) {
const res = await this.songService.song(
{
id,
},
mapInclude(include, req, SongController.includableFields),
);
if (res === null) throw new NotFoundException('Song not found');
return res;
@@ -138,7 +198,13 @@ export class SongController {
@Get(':id/history')
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@ApiOperation({
description: 'get the history of the connected user on a specific song',
})
@ApiOkResponse({
type: SongHistoryResult,
description: 'Records of previous games of the user',
})
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
return this.historyService.getForSong({

View File

@@ -6,6 +6,14 @@ import { PrismaService } from 'src/prisma/prisma.service';
export class SongService {
constructor(private prisma: PrismaService) {}
async songByArtist(data: number): Promise<Song[]> {
return this.prisma.song.findMany({
where: {
artistId: { equals: data },
},
});
}
async createSong(data: Prisma.SongCreateInput): Promise<Song> {
return this.prisma.song.create({
data,
@@ -14,9 +22,11 @@ export class SongService {
async song(
songWhereUniqueInput: Prisma.SongWhereUniqueInput,
include?: Prisma.SongInclude,
): Promise<Song | null> {
return this.prisma.song.findUnique({
where: songWhereUniqueInput,
include,
});
}
@@ -26,14 +36,16 @@ export class SongService {
cursor?: Prisma.SongWhereUniqueInput;
where?: Prisma.SongWhereInput;
orderBy?: Prisma.SongOrderByWithRelationInput;
include?: Prisma.SongInclude;
}): Promise<Song[]> {
const { skip, take, cursor, where, orderBy } = params;
const { skip, take, cursor, where, orderBy, include } = params;
return this.prisma.song.findMany({
skip,
take,
cursor,
where,
orderBy,
include,
});
}

View File

@@ -1,6 +1,13 @@
import { Controller, Get, Param, NotFoundException, Response } from '@nestjs/common';
import {
Controller,
Get,
Post,
Param,
NotFoundException,
Response,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { User } from 'src/models/user';
@ApiTags('users')
@@ -22,6 +29,9 @@ export class UsersController {
}
@Get(':id/picture')
@ApiOkResponse({
description: 'Return the profile picture of the requested user',
})
async getPicture(@Response() res: any, @Param('id') id: number) {
return await this.usersService.getProfilePicture(+id, res);
}

View File

@@ -2,7 +2,6 @@ import {
Injectable,
InternalServerErrorException,
NotFoundException,
StreamableFile,
} from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
@@ -53,7 +52,7 @@ export class UsersService {
username: `Guest ${randomUUID()}`,
isGuest: true,
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
email: '',
email: null,
password: '',
},
});
@@ -89,14 +88,32 @@ export class UsersService {
// We could not find a profile icon locally, using gravatar instead.
const user = await this.user({ id: userId });
if (!user) throw new InternalServerErrorException();
if (!user.email) throw new NotFoundException();
const hash = createHash('md5')
.update(user.email.trim().toLowerCase())
.digest('hex');
const resp = await fetch(
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
);
for (const [k, v] of resp.headers)
resp.headers.set(k, v);
for (const [k, v] of resp.headers) resp.headers.set(k, v);
resp.body!.pipe(res);
}
async addLikedSong(userId: number, songId: number) {
return this.prisma.likedSongs.create({
data: { songId: songId, userId: userId },
});
}
async getLikedSongs(userId: number) {
return this.prisma.likedSongs.findMany({
where: { userId: userId },
});
}
async removeLikedSong(userId: number, songId: number) {
return this.prisma.likedSongs.deleteMany({
where: { userId: userId, songId: songId },
});
}
}

33
back/src/utils/include.ts Normal file
View File

@@ -0,0 +1,33 @@
import { Request } from 'express';
import { BadRequestException } from '@nestjs/common';
export type IncludeMap<IncludeType> = {
[key in keyof IncludeType]:
| boolean
| ((ctx: { user: { id: number; username: string } }) => IncludeType[key]);
};
export function mapInclude<IncludeType>(
include: string | undefined,
req: Request,
fields: IncludeMap<IncludeType>,
): IncludeType | undefined {
if (!include) return undefined;
const ret: IncludeType = {} as IncludeType;
for (const key of include.split(',')) {
const value =
typeof fields[key] === 'function'
? fields[key]({ user: req.user })
: fields[key];
if (value !== false && value !== undefined) ret[key] = value;
else {
throw new BadRequestException(
`Invalid include, ${key} is not valid. Valid includes are: ${Object.keys(
fields,
).join(', ')}.`,
);
}
}
return ret;
}

View File

@@ -30,7 +30,7 @@ Register Duplicates
# We can't use the `Register` keyword because it assert for success
POST /auth/register {"username": "user-duplicate", "password": "pass", "email": "mail@kyoo.moe"}
Output
Integer response status 400
Integer response status 409
Login user-duplicate
[Teardown] DELETE /auth/me

View File

@@ -3,6 +3,7 @@ Documentation Tests of the /song route.
... Ensures that the songs CRUD works corectly.
Resource ../rest.resource
Resource ../auth/auth.resource
*** Test Cases ***
@@ -133,5 +134,47 @@ Get midi file
Integer response status 201
GET /song/${res.body.id}/midi
Integer response status 200
#Output
# Output
[Teardown] DELETE /song/${res.body.id}
Find a song with artist
[Documentation] Create a song and find it with it's artist
&{res2}= POST /artist { "name": "Tghjmk"}
Output
Integer response status 201
&{res}= POST
... /song
... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"}
Output
Integer response status 201
&{get}= GET /song/${res.body.id}?include=artist
Output
Integer response status 200
Should Be Equal ${res2.body} ${get.body.artist}
[Teardown] Run Keywords DELETE /song/${res.body.id}
... AND DELETE /artist/${res2.body.id}
Find a song with artist and history
[Documentation] Create a song and find it with it's artist
${userID}= RegisterLogin wowusersfkj
&{res2}= POST /artist { "name": "Tghjmk"}
Output
Integer response status 201
&{res}= POST
... /song
... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"}
Output
Integer response status 201
&{res3}= POST
... /history
... { "songID": ${res.body.id}, "userID": ${userID}, "score": 12, "difficulties": {}, "info": {} }
Output
Integer response status 201
&{get}= GET /song/${res.body.id}?include=artist,SongHistory
Output
Integer response status 200
Should Be Equal ${res2.body} ${get.body.artist}
Should Be Equal ${res3.body} ${get.body.SongHistory[0]}
[Teardown] Run Keywords DELETE /auth/me
... AND DELETE /song/${res.body.id}
... AND DELETE /artist/${res2.body.id}

37
config/logs_nginx.conf Normal file
View File

@@ -0,0 +1,37 @@
user nginx;
worker_processes 5; ## Default: 1
events {
worker_connections 1000;
}
http {
resolver 127.0.0.11;
server {
listen 3100;
location = / {
return 200 'OK';
auth_basic off;
}
location = /api/prom/push {
proxy_pass http://write:3100\$$request_uri;
}
location = /api/prom/tail {
proxy_pass http://read:3100\$$request_uri;
proxy_set_header Upgrade \$$http_upgrade;
proxy_set_header Connection "upgrade";
}
location ~ /api/prom/.* {
proxy_pass http://read:3100\$$request_uri;
}
location = /loki/api/v1/push {
proxy_pass http://write:3100\$$request_uri;
}
location = /loki/api/v1/tail {
proxy_pass http://read:3100\$$request_uri;
proxy_set_header Upgrade \$$http_upgrade;
proxy_set_header Connection "upgrade";
}
location ~ /loki/api/.* {
proxy_pass http://read:3100\$$request_uri;
}
}
}

36
config/loki-config.yaml Normal file
View File

@@ -0,0 +1,36 @@
---
auth_enabled: false
server:
http_listen_port: 3100
memberlist:
join_members:
- loki:7946
schema_config:
configs:
- from: 2021-08-01
store: boltdb-shipper
object_store: s3
schema: v11
index:
prefix: index_
period: 24h
common:
path_prefix: /loki
replication_factor: 1
storage:
s3:
endpoint: minio:9000
insecure: true
bucketnames: loki-data
access_key_id: loki
secret_access_key: 12345678
s3forcepathstyle: true
ring:
kvstore:
store: memberlist
query_range:
parallelise_shardable_queries: false
ruler:
storage:
s3:
bucketnames: loki-ruler

View File

@@ -0,0 +1,22 @@
---
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://gateway:3100/loki/api/v1/push
tenant_id: tenant1
scrape_configs:
- job_name: flog_scrape
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'

View File

@@ -1,3 +1,10 @@
networks:
loki:
volumes:
scoro_logs:
services:
back:
build:
@@ -25,6 +32,9 @@ services:
volumes:
- ./scorometer:/app
- ./assets:/assets
- scoro_logs:/logs
networks:
- loki
db:
container_name: db
@@ -40,13 +50,14 @@ services:
retries: 5
ports:
- "5432:5432"
front:
build:
context: ./front
dockerfile: Dockerfile.dev
environment:
- SCOROMETER_URL=http://scorometer:6543/
- NGINX_PORT=80
- NGINX_PORT=4567
ports:
- "19006:19006"
volumes:
@@ -55,3 +66,20 @@ services:
- "back"
env_file:
- .env
nginx:
image: nginx
environment:
- API_URL=${API_URL:-http://back:3000}
- SCOROMETER_URL=${SCOROMETER_URL:-http://scorometer:6543}
- FRONT_URL=${FRONT_URL:-http://front:19006}
- PORT=4567
depends_on:
- back
- front
- scorometer
volumes:
- "./front/assets:/assets:ro"
- "./front/nginx.conf.template.dev:/etc/nginx/templates/default.conf.template:ro"
ports:
- "4567:4567"

185
docker-compose.log.yml Normal file
View File

@@ -0,0 +1,185 @@
services:
read:
image: grafana/loki:2.8.2
command: "-config.file=/etc/loki/config.yaml -target=read"
ports:
- 3101:3100
- 7946
- 9095
volumes:
- ./config/loki-config.yaml:/etc/loki/config.yaml
depends_on:
- minio
healthcheck:
test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1" ]
interval: 10s
timeout: 5s
retries: 5
networks: &loki-dns
loki:
aliases:
- loki
write:
image: grafana/loki:2.8.2
command: "-config.file=/etc/loki/config.yaml -target=write"
ports:
- 3102:3100
- 7946
- 9095
volumes:
- ./config/loki-config.yaml:/etc/loki/config.yaml
healthcheck:
test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1" ]
interval: 10s
timeout: 5s
retries: 5
depends_on:
- minio
networks:
<<: *loki-dns
promtail:
image: grafana/promtail:2.8.2
volumes:
- ./config/promtail-local-config.yaml:/etc/promtail/config.yaml:ro
- /var/run/docker.sock:/var/run/docker.sock
command: -config.file=/etc/promtail/config.yaml
depends_on:
- gateway
networks:
- loki
minio:
image: minio/minio:RELEASE.2023-07-21T21-12-44Z
entrypoint:
- sh
- -euc
- |
mkdir -p /data/loki-data && \
mkdir -p /data/loki-ruler && \
minio server /data
environment:
- MINIO_ROOT_USER=loki
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
- MINIO_PROMETHEUS_AUTH_TYPE=public
- MINIO_UPDATE=off
ports:
- 9000
volumes:
- ./.data/minio:/data
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ]
interval: 15s
timeout: 20s
retries: 5
networks:
- loki
grafana:
image: grafana/grafana:9.5.6
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
depends_on:
- gateway
entrypoint:
- sh
- -euc
- |
mkdir -p /etc/grafana/provisioning/datasources
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://gateway:3100
jsonData:
httpHeaderName1: "X-Scope-OrgID"
secureJsonData:
httpHeaderValue1: "tenant1"
EOF
/run.sh
ports:
- "3001:3000"
volumes:
- ./grafana/dashboard.yaml:/etc/grafana/provisioning/dashboards/main.yaml
- ./grafana/dashboards:/var/lib/grafana/dashboards
healthcheck:
test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3001/api/health || exit 1" ]
interval: 10s
timeout: 5s
retries: 5
networks:
- loki
gateway:
image: nginx:1.25.1
depends_on:
- read
- write
entrypoint:
- sh
- -euc
- |
cat <<EOF > /etc/nginx/nginx.conf
user nginx;
worker_processes 5; ## Default: 1
events {
worker_connections 1000;
}
http {
resolver 127.0.0.11;
server {
listen 3100;
location = / {
return 200 'OK';
auth_basic off;
}
location = /api/prom/push {
proxy_pass http://write:3100\$$request_uri;
}
location = /api/prom/tail {
proxy_pass http://read:3100\$$request_uri;
proxy_set_header Upgrade \$$http_upgrade;
proxy_set_header Connection "upgrade";
}
location ~ /api/prom/.* {
proxy_pass http://read:3100\$$request_uri;
}
location = /loki/api/v1/push {
proxy_pass http://write:3100\$$request_uri;
}
location = /loki/api/v1/tail {
proxy_pass http://read:3100\$$request_uri;
proxy_set_header Upgrade \$$http_upgrade;
proxy_set_header Connection "upgrade";
}
location ~ /loki/api/.* {
proxy_pass http://read:3100\$$request_uri;
}
}
}
EOF
/docker-entrypoint.sh nginx -g "daemon off;"
ports:
- "3100:3100"
healthcheck:
test: ["CMD", "service", "nginx", "status"]
interval: 10s
timeout: 5s
retries: 5
networks:
- loki

View File

@@ -1,3 +1,9 @@
networks:
loki:
volumes:
scoro_logs:
services:
back:
image: ghcr.io/chroma-case/back:main
@@ -16,13 +22,14 @@ services:
ports:
- "6543:6543"
volumes:
- scoro_logs:/logs
- ./assets:/assets
db:
container_name: db
image: postgres:alpine3.14
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORDgrafana}
- POSTGRES_DB=${POSTGRES_DB}
ports:
- "5432:5432"
@@ -43,4 +50,4 @@ services:
depends_on:
- "back"
env_file:
- .env
- .env

View File

@@ -1,3 +1,12 @@
networks:
loki:
volumes:
db:
scoro_logs:
services:
back:
build: ./back
@@ -17,6 +26,7 @@ services:
- "6543:6543"
volumes:
- ./assets:/assets
- scoro_logs:/logs
db:
container_name: db
image: postgres:alpine3.14
@@ -35,11 +45,7 @@ services:
retries: 5
front:
build:
context: ./front
args:
- API_URL=${API_URL}
- SCORO_URL=${SCORO_URL}
build: ./front
environment:
- API_URL=http://back:3000/
- SCOROMETER_URL=http://scorometer:6543/
@@ -49,7 +55,4 @@ services:
depends_on:
- "back"
env_file:
- .env
volumes:
db:
- .env

43
flake.lock generated
View File

@@ -1,43 +0,0 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1665573177,
"narHash": "sha256-Arkrf3zmi3lXYpbSe9H+HQxswQ6jxsAmeQVq5Sr/OZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d2afb051ffd904af5a825f58abee3c63b148c5f2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,31 +0,0 @@
{
description = "A prisma test project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/master";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShell = pkgs.mkShell {
nativeBuildInputs = [ pkgs.bashInteractive ];
buildInputs = with pkgs; [
nodePackages.prisma
nodePackages."@nestjs/cli"
nodePackages.npm
nodejs-slim
yarn
python3
pkg-config
];
shellHook = with pkgs; ''
export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine"
export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine"
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node"
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine"
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt"
export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase
'';
};
});
}

View File

@@ -1,4 +1,7 @@
node_modules/
.expo/
.idea/
.vscode/
.vscode/
.dockerignore
Dockerfile
Dockerfile.dev

31
front/.gitignore vendored
View File

@@ -1,18 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
npm-debug.*
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
*.apk
yarn.error*
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
.idea/
.expo
# local env files
.env*.local
# typescript
*.tsbuildinfo

View File

@@ -4,10 +4,10 @@ import Chapter from './models/Chapter';
import Lesson from './models/Lesson';
import Genre, { GenreHandler } from './models/Genre';
import LessonHistory from './models/LessonHistory';
import likedSong, { LikedSongHandler } from './models/LikedSong';
import Song, { SongHandler } from './models/Song';
import { SongHistoryHandler, SongHistoryItem, SongHistoryItemHandler } from './models/SongHistory';
import User, { UserHandler } from './models/User';
import Constants from 'expo-constants';
import store from './state/Store';
import { Platform } from 'react-native';
import { en } from './i18n/Translations';
@@ -21,7 +21,7 @@ import { PlageHandler } from './models/Plage';
import { ListHandler } from './models/List';
import { AccessTokenResponseHandler } from './models/AccessTokenResponse';
import * as yup from 'yup';
import { base64ToBlob } from 'file64';
import { base64ToBlob } from './utils/base64ToBlob';
import { ImagePickerAsset } from 'expo-image-picker';
type AuthenticationInput = { username: string; password: string };
@@ -66,9 +66,7 @@ export class ValidationError extends Error {
export default class API {
public static readonly baseUrl =
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
? '/api'
: Constants.manifest?.extra?.apiUrl;
Platform.OS === 'web' ? '/api' : process.env.EXPO_PUBLIC_API_URL!;
public static async fetch(
params: FetchParams,
handle: Pick<Required<HandleParams>, 'raw'>
@@ -97,8 +95,16 @@ export default class API {
});
if (!handle || handle.emptyResponse) {
if (!response.ok) {
console.log(await response.json());
throw new APIError(response.statusText, response.status);
let responseMessage = response.statusText;
try {
const responseData = await response.json();
console.log(responseData);
if (responseData.message) responseMessage = responseData.message;
} catch (e) {
console.log(e);
throw new APIError(response.statusText, response.status, 'unknownError');
}
throw new APIError(responseMessage, response.status, 'unknownError');
}
return;
}
@@ -110,7 +116,7 @@ export default class API {
try {
const jsonResponse = JSON.parse(body);
if (!response.ok) {
throw new APIError(response.statusText ?? body, response.status);
throw new APIError(response.statusText ?? body, response.status, 'unknownError');
}
const validated = await handler.validator.validate(jsonResponse).catch((e) => {
if (e instanceof yup.ValidationError) {
@@ -287,6 +293,43 @@ export default class API {
),
};
}
/**
* @description retrieves songs from a specific artist
* @param artistId is the id of the artist that composed the songs aimed
* @returns a Promise of Songs type array
*/
public static getSongsByArtist(artistId: number): Query<Song[]> {
return {
key: ['artist', artistId, 'songs'],
exec: () =>
API.fetch(
{
route: `/song?artistId=${artistId}`,
},
{ handler: PlageHandler(SongHandler) }
).then(({ data }) => data),
};
}
/**
* Retrieves all songs corresponding to the given genre ID
* @param genreId the id of the genre we're aiming
* @returns a promise of an array of Songs
*/
public static getSongsByGenre(genreId: number): Query<Song[]> {
return {
key: ['genre', genreId, 'songs'],
exec: () =>
API.fetch(
{
route: `/song?genreId=${genreId}`,
},
{ handler: PlageHandler(SongHandler) }
).then(({ data }) => data),
};
}
/**
* Retrive a song's midi partition
* @param songId the id to find the song
@@ -322,6 +365,23 @@ export default class API {
return `${API.baseUrl}/genre/${genreId}/illustration`;
}
/**
* Retrieves a genre
* @param genreId the id of the aimed genre
*/
public static getGenre(genreId: number): Query<Genre> {
return {
key: ['genre', genreId],
exec: () =>
API.fetch(
{
route: `/genre/${genreId}`,
},
{ handler: GenreHandler }
),
};
}
/**
* Retrive a song's musicXML partition
* @param songId the id to find the song
@@ -608,4 +668,31 @@ export default class API {
formData,
});
}
public static async addLikedSong(songId: number): Promise<void> {
await API.fetch({
route: `/auth/me/likes/${songId}`,
method: 'POST',
});
}
public static async removeLikedSong(songId: number): Promise<void> {
await API.fetch({
route: `/auth/me/likes/${songId}`,
method: 'DELETE',
});
}
public static getLikedSongs(): Query<likedSong[]> {
return {
key: ['liked songs'],
exec: () =>
API.fetch(
{
route: '/auth/me/likes',
},
{ handler: ListHandler(LikedSongHandler) }
),
};
}
}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import store, { persistor } from './state/Store';
@@ -10,12 +10,22 @@ import LanguageGate from './i18n/LanguageGate';
import ThemeProvider, { ColorSchemeProvider } from './Theme';
import 'react-native-url-polyfill/auto';
import { QueryRules } from './Queries';
import { useFonts } from 'expo-font';
const queryClient = new QueryClient(QueryRules);
export default function App() {
SplashScreen.preventAutoHideAsync();
setTimeout(SplashScreen.hideAsync, 500);
const [fontsLoaded] = useFonts({
Lexend: require('./assets/fonts/lexend.ttf'),
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
return (
<Provider store={store}>

View File

@@ -4,24 +4,24 @@
FROM node:16-alpine as build
WORKDIR /app
# install expo cli
RUN yarn global add expo-cli@6.0.5
RUN yarn global add expo-cli
# add sharp-cli (^2.1.0) for faster image processing
RUN yarn global add sharp-cli@^2.1.0
RUN yarn global add sharp-cli
COPY package.json yarn.lock ./
RUN yarn install
RUN yarn install --immutable
RUN expo install
COPY . .
ARG API_URL
ENV API_URL=$API_URL
ENV EXPO_PUBLIC_API_URL=$API_URL
ARG SCORO_URL
ENV SCORO_URL=$SCORO_URL
ENV EXPO_PUBLIC_API_URL=$SCORO_URL
RUN yarn tsc && expo build:web
RUN yarn tsc && npx expo export:web
# Serve the app
FROM nginx:1.21-alpine
COPY --from=build /app/web-build /usr/share/nginx/html
COPY nginx.conf.template /etc/nginx/conf.d/default.conf.template
CMD envsubst '$API_URL $SCOROMETER_URL $NGINX_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'
COPY ./assets/ /usr/share/nginx/html/assets/
COPY nginx.conf.template /etc/nginx/templates/default.conf.template

View File

@@ -11,7 +11,6 @@ import { RootState, useSelector } from './state/Store';
import { useDispatch } from 'react-redux';
import { Translate, translate } from './i18n/i18n';
import SongLobbyView from './views/SongLobbyView';
import AuthenticationView from './views/AuthenticationView';
import StartPageView from './views/StartPageView';
import HomeView from './views/HomeView';
import SearchView from './views/SearchView';
@@ -28,7 +27,14 @@ import { Button, Center, VStack } from 'native-base';
import { unsetAccessToken } from './state/UserSlice';
import TextButton from './components/TextButton';
import ErrorView from './views/ErrorView';
import GenreDetailsView from './views/GenreDetailsView';
import GoogleView from './views/GoogleView';
import VerifiedView from './views/VerifiedView';
import SigninView from './views/SigninView';
import SignupView from './views/SignupView';
import TabNavigation from './components/V2/TabNavigation';
import PasswordResetView from './views/PasswordResetView';
import ForgotPasswordView from './views/ForgotPasswordView';
// Util function to hide route props in URL
const removeMe = () => '';
@@ -40,6 +46,11 @@ const protectedRoutes = () =>
options: { title: translate('welcome'), headerLeft: null },
link: '/',
},
HomeNew: {
component: TabNavigation,
options: { headerShown: false },
link: '/V2',
},
Play: { component: PlayView, options: { title: translate('play') }, link: '/play/:songId' },
Settings: {
component: SetttingsNavigator,
@@ -59,6 +70,11 @@ const protectedRoutes = () =>
options: { title: translate('artistFilter') },
link: '/artist/:artistId',
},
Genre: {
component: GenreDetailsView,
options: { title: translate('genreFilter') },
link: '/genre/:genreId',
},
Score: {
component: ScoreView,
options: { title: translate('score'), headerLeft: null },
@@ -75,7 +91,12 @@ const protectedRoutes = () =>
link: undefined,
},
User: { component: ProfileView, options: { title: translate('user') }, link: '/user' },
} as const);
Verified: {
component: VerifiedView,
options: { title: 'Verify email', headerShown: false },
link: '/verify',
},
}) as const;
const publicRoutes = () =>
({
@@ -85,28 +106,31 @@ const publicRoutes = () =>
link: '/',
},
Login: {
component: (params: RouteProps<{}>) =>
AuthenticationView({ isSignup: false, ...params }),
options: { title: translate('signInBtn') },
component: SigninView,
options: { title: translate('signInBtn'), headerShown: false },
link: '/login',
},
Signup: {
component: (params: RouteProps<{}>) =>
AuthenticationView({ isSignup: true, ...params }),
options: { title: translate('signUpBtn') },
component: SignupView,
options: { title: translate('signUpBtn'), headerShown: false },
link: '/signup',
},
Oops: {
component: ProfileErrorView,
options: { title: 'Oops', headerShown: false },
link: undefined,
},
Google: {
component: GoogleView,
options: { title: 'Google signin', headerShown: false },
link: '/logged/google',
},
} as const);
PasswordReset: {
component: PasswordResetView,
options: { title: 'Password reset form', headerShown: false },
link: '/password_reset',
},
ForgotPassword: {
component: ForgotPasswordView,
options: { title: 'Password reset form', headerShown: false },
link: '/forgot_password',
},
}) as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Route<Props = any> = {
@@ -127,19 +151,18 @@ type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never }>();
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never; Oops: never }>();
const RouteToScreen =
<T extends {}>(component: Route<T>['component']) =>
// eslint-disable-next-line react/display-name
(props: NativeStackScreenProps<T & ParamListBase>) =>
(
<>
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
);
(props: NativeStackScreenProps<T & ParamListBase>) => (
<>
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
);
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =>
Object.entries(routes).map(([name, route], routeIndex) => (
@@ -176,6 +199,8 @@ const routesToLinkingConfig = (
const ProfileErrorView = (props: { onTryAgain: () => void }) => {
const dispatch = useDispatch();
const navigation = useNavigation();
return (
<Center style={{ flexGrow: 1 }}>
<VStack space={3}>
@@ -184,7 +209,10 @@ const ProfileErrorView = (props: { onTryAgain: () => void }) => {
<Translate translationKey="tryAgain" />
</Button>
<TextButton
onPress={() => dispatch(unsetAccessToken())}
onPress={() => {
dispatch(unsetAccessToken());
navigation.navigate('Start');
}}
colorScheme="error"
variant="outline"
translate={{ translationKey: 'signOutBtn' }}
@@ -245,12 +273,15 @@ export const Router = () => {
>
<Stack.Navigator>
{authStatus == 'error' ? (
<Stack.Screen
name="Oops"
component={RouteToScreen(() => (
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
))}
/>
<>
<Stack.Screen
name="Oops"
component={RouteToScreen(() => (
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
))}
/>
{routesToScreens(publicRoutes())}
</>
) : (
routesToScreens(routes)
)}

View File

@@ -12,63 +12,118 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
useSystemColorMode: false,
initialColorMode: colorScheme,
},
fonts: {
heading: 'Lexend',
body: 'Lexend',
mono: 'Lexend',
},
colors: {
primary: {
50: '#e6faea',
100: '#c8e7d0',
200: '#a7d6b5',
300: '#86c498',
400: '#65b47c',
500: '#4b9a62',
600: '#3a784b',
700: '#275635',
800: '#14341f',
900: '#001405',
50: '#eff1fe',
100: '#e7eafe',
200: '#cdd4fd',
300: '#5f74f7',
400: '#5668de',
500: '#4c5dc6',
600: '#4757b9',
700: '#394694',
800: '#2b346f',
900: '#212956',
},
secondary: {
50: '#d8ffff',
100: '#acffff',
200: '#7dffff',
300: '#4dffff',
400: '#28ffff',
500: '#18e5e6',
600: '#00b2b3',
700: '#007f80',
800: '#004d4e',
900: '#001b1d',
50: '#f7f3ff',
100: '#f3edfe',
200: '#e6d9fe',
300: '#ae84fb',
400: '#9d77e2',
500: '#8b6ac9',
600: '#8363bc',
700: '#684f97',
800: '#4e3b71',
900: '#3d2e58',
},
error: {
50: '#ffe2e9',
100: '#ffb1bf',
200: '#ff7f97',
300: '#ff4d6d',
400: '#fe1d43',
500: '#e5062b',
600: '#b30020',
700: '#810017',
800: '#4f000c',
900: '#200004',
50: '#f7f3ff',
100: '#f3edfe',
200: '#e6d9fe',
300: '#ae84fb',
400: '#9d77e2',
500: '#8b6ac9',
600: '#8363bc',
700: '#684f97',
800: '#4e3b71',
900: '#3d2e58',
},
alert: {
50: '#fff2f1',
100: '#ffebea',
200: '#ffd6d3',
300: '#ff7a72',
400: '#e66e67',
500: '#cc625b',
600: '#bf5c56',
700: '#994944',
800: '#733733',
900: '#592b28',
},
notification: {
50: '#ffe1e1',
100: '#ffb1b1',
200: '#ff7f7f',
300: '#ff4c4c',
400: '#ff1a1a',
500: '#e60000',
600: '#b40000',
700: '#810000',
800: '#500000',
900: '#210000',
50: '#fdfbec',
100: '#fcf9e2',
200: '#f8f3c3',
300: '#ead93c',
400: '#d3c336',
500: '#bbae30',
600: '#b0a32d',
700: '#8c8224',
800: '#69621b',
900: '#524c15',
},
black: {
50: '#e7e7e8',
100: '#dbdbdc',
200: '#b5b5b6',
300: '#101014',
400: '#0e0e12',
500: '#0d0d10',
600: '#0c0c0f',
700: '#0a0a0c',
800: '#070709',
900: '#060607',
},
red: {
50: '#fdedee',
100: '#fce4e5',
200: '#f9c7c9',
300: '#ed4a51',
400: '#d54349',
500: '#be3b41',
600: '#b2383d',
700: '#8e2c31',
800: '#6b2124',
900: '#531a1c',
},
},
components: {
Button: {
variants: {
solid: () => ({
rounded: 'full',
}),
baseStyle: () => ({
borderRadius: 'md',
}),
},
Link: {
defaultProps: {
isUnderlined: false,
},
baseStyle: () => ({
_text: {
color: 'secondary.300',
},
_hover: {
isUnderlined: true,
_text: {
color: 'secondary.400',
},
},
}),
},
},
})}

15
front/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
# Bundle artifacts
*.jsbundle

View File

@@ -0,0 +1,180 @@
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
// Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
// codegenDir = file("../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
namespace 'build.apk'
defaultConfig {
applicationId 'build.apk'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString())
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
// Split option: 'foo,bar' -> ['foo', 'bar']
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
// Trim all elements in place.
for (i in 0..<options.size()) options[i] = options[i].trim();
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
options -= ""
if (options.length > 0) {
println "android.packagingOptions.$prop += $options ($options.length)"
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
options.each {
android.packagingOptions[prop] += it
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
def frescoVersion = rootProject.ext.frescoVersion
// If your app supports Android versions before Ice Cream Sandwich (API level 14)
if (isGifEnabled || isWebpEnabled) {
implementation("com.facebook.fresco:fresco:${frescoVersion}")
implementation("com.facebook.fresco:imagepipeline-okhttp3:${frescoVersion}")
}
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${frescoVersion}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${frescoVersion}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${frescoVersion}")
}
}
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesAppBuildGradle(project)

Binary file not shown.

14
front/android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,14 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -0,0 +1,33 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:theme="@style/AppTheme">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="49.0.0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="build.apk"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@@ -0,0 +1,3 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1 @@
<resources/>

View File

@@ -0,0 +1,6 @@
<resources>
<color name="splashscreen_background">#ffffff</color>
<color name="iconBackground">#FFFFFF</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#ffffff</color>
</resources>

View File

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">Chromacase</string>
<string name="expo_splash_screen_resize_mode" translatable="false">cover</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources>

View File

@@ -0,0 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
</style>
<style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/splashscreen</item>
</style>
</resources>

View File

@@ -0,0 +1,40 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '33.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '33')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.8.10'
frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0'
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath('com.android.tools.build:gradle:7.4.2')
classpath('com.facebook.react:react-native-gradle-plugin')
}
}
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
}
maven {
// Android JSC is installed from npm
url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist'))
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}

Some files were not shown because too many files have changed in this diff Show More