164 Commits

Author SHA1 Message Date
GitBluub
cfd4a8acec sort by rating and slug and normal title 2024-01-15 01:29:05 +01:00
GitBluub
e63789cbc1 fix: skip if no artist or song name 2023-11-13 22:55:22 +01: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
Clément Le Bihan
e0f2674811 fix pr 2023-09-06 15:09:54 +02:00
Clément Le Bihan
b84ee11f45 Fix de arthur 2023-09-06 15:09:54 +02:00
Clément Le Bihan
a2494ce498 prettied 2023-09-06 15:09:54 +02:00
Clément Le Bihan
b76d496034 fix ts issues 2 2023-09-06 15:09:54 +02:00
Clément Le Bihan
a81d3ee34d fixed ts type issue 2023-09-06 15:09:54 +02:00
Clément Le Bihan
85473ae492 Removed old commented react useState 2023-09-06 15:09:54 +02:00
Clément Le Bihan
9655e986ff Removed old code from HomeView and auto format some files 2023-09-06 15:09:54 +02:00
Clément Le Bihan
101ea8498b removing old code commented and unused dependancies 2023-09-06 15:09:54 +02:00
Clément Le Bihan
7d33f85cbc Cleanup parition view 2023-09-06 15:09:54 +02:00
Clément Le Bihan
66d792715e Removed Parition context declaration/init 2023-09-06 15:09:54 +02:00
Clément Le Bihan
40581f4a45 Removed the timestamp partition context to reuse normal props clean up console logs and now displaying a toast to tell is the scorometer crashed 2023-09-06 15:09:54 +02:00
Clément Le Bihan
2ca3fcb81a reactivating websocket connection but error view appear when it shouldn't 2023-09-06 15:09:54 +02:00
Clément Le Bihan
30fcacbec6 Now using redux to not create sound player every time the phaser is also implicitely cached 2023-09-06 15:09:54 +02:00
Clément Le Bihan
7c3289ccec now phasercanvas makes sounds used the same stack as previously and ram issue spotted 2023-09-06 15:09:54 +02:00
Clément Le Bihan
7438986bcd Cursor is controlled by partition timestamps provided by playview and can thus be paused and onEndReached is now called 2023-09-06 15:09:54 +02:00
Clément Le Bihan
3ac017a5f0 Cursor with cam follow is moving to correct notes, timing is fake 2023-09-06 15:09:54 +02:00
Clément Le Bihan
8e5cc1bc44 Added sliding to the partition but some issues 2023-09-06 15:09:54 +02:00
Clément Le Bihan
125a7faf02 early Experiment working 2023-09-06 15:09:54 +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
c81f8df61c prisma migration + back auth/me/likes + front API add and get methods for liked song 2023-08-30 13:06:25 +02:00
Arthur Jamet
a3676fabf8 Front: Update User Avatar (#250)
* Front: Update User Avatar

* Front: Fix expo-image-picker version
2023-08-07 10:28:55 +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
Arthur Jamet
9f542fc9dd Front: User Avatar 2023-07-26 21:00:41 +09:00
930191569f Fix upload file issue 2023-07-26 21:00:41 +09:00
74cd9c0df2 Remove a usless validator 2023-07-26 21:00:41 +09:00
d2642b4fb8 Fixing gravatar 2023-07-26 21:00:41 +09:00
ebcc48cc57 Upgrade back packages 2023-07-26 21:00:41 +09:00
95b08935cc Add file upload 2023-07-26 21:00:41 +09:00
04487c9b24 Add get profile route that supports gravatar 2023-07-26 21:00:41 +09:00
Arthur Jamet
20eb62d19b Front: Graphes de Score (#248) 2023-07-26 12:00:06 +01:00
Zoe Roux
567d3250e2 Merge pull request #234 from Chroma-Case/feat/google 2023-07-24 19:40:15 +09:00
4207d5ee50 Try to fix the CI 2023-07-24 19:33:25 +09:00
GitBluub
509cc5b9f8 rename musics 2023-07-24 14:51:19 +09:00
GitBluub
1b22dba9cd rename musics 2023-07-24 14:44:55 +09:00
GitBluub
2ec95dd3c3 wip 2023-07-23 18:14:11 +09:00
Arthur Jamet
c0d9ee7ca6 Front: Merge 2023-07-16 18:11:34 +01:00
Arthur Jamet
27f7945289 Front: Use React-Native feature to handle Google Redirections 2023-06-29 15:02:06 +01:00
GitBluub
5a190f3b96 wip 2023-06-28 22:03:59 +09:00
Arthur Jamet
3d76834f45 Front: Add Missing Translation + Prettier 2023-06-26 15:00:35 +01:00
ccc86895e2 Add an indicator of the google account on the front 2023-06-26 22:41:07 +09:00
279d16d59a Add google things on the front 2023-06-26 22:38:59 +09:00
04d288b844 Add google signin/signup 2023-06-26 22:38:59 +09:00
200 changed files with 24714 additions and 11746 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,4 +7,11 @@ JWT_SECRET=wow
POSTGRES_DB=chromacase
API_URL=http://localhost:80/api
SCORO_URL=ws://localhost:6543
MINIO_ROOT_PASSWORD=12345678
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

@@ -84,16 +84,7 @@ jobs:
fetch-depth: 0
- name: Copy env file to github secret env file
run: |
touch .env
echo "POSTGRES_USER=user" >> .env
echo "POSTGRES_PASSWORD=eip" >> .env
echo "POSTGRES_NAME=chromacase" >> .env
echo "POSTGRES_HOST=db" >> .env
echo "DATABASE_URL=postgresql://user:eip@db:5432/chromacase" >> .env
echo "JWT_SECRET=wow" >> .env
echo "POSTGRES_DB=chromacase" >> .env
echo "API_URL=http://localhost:80/api" >> .env
run: cp .env.example .env
- name: Start the service
run: docker-compose up -d back db
@@ -101,7 +92,8 @@ jobs:
- name: Perform healthchecks
run: |
docker-compose ps -a
wget --retry-connrefused http://localhost:3000 # /healthcheck
docker-compose logs
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

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 376 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,4 +1,4 @@
#!/bin/env python3
#!/usr/bin/env python3
import sys
import os

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

19482
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,51 +21,61 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.1.0",
"@nestjs/core": "^8.0.0",
"@nestjs/jwt": "^8.0.1",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/common": "^10.1.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.1.0",
"@nestjs/jwt": "^10.1.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^8.2.2",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/swagger": "^5.2.1",
"@prisma/client": "^4.4.0",
"@nestjs/passport": "^10.0.0",
"@nestjs/platform-express": "^10.1.0",
"@nestjs/swagger": "^7.1.2",
"@prisma/client": "^5.0.0",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/passport": "^1.0.9",
"@types/passport": "^1.0.12",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"passport-jwt": "^4.0.0",
"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": "^3.0.2",
"rxjs": "^7.2.0",
"swagger-ui-express": "^4.5.0"
"rimraf": "^5.0.1",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/express": "^4.17.13",
"@types/jest": "27.4.1",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.2.5",
"prettier": "^2.3.2",
"prisma": "^4.4.0",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typescript": "^4.3.5"
"@nestjs/cli": "^10.1.10",
"@nestjs/schematics": "^10.0.1",
"@nestjs/testing": "^10.1.0",
"@types/express": "^4.17.17",
"@types/jest": "29.5.3",
"@types/multer": "^1.4.7",
"@types/node": "^20.4.4",
"@types/nodemailer": "^6.4.9",
"@types/passport-google-oauth20": "^2.0.11",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.1",
"prettier": "^3.0.0",
"prisma": "^5.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.6"
},
"jest": {
"moduleFileExtensions": [

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[googleID]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "googleID" TEXT,
ALTER COLUMN "password" DROP NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "User_googleID_key" ON "User"("googleID");

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")
@@ -12,14 +18,26 @@ datasource db {
model User {
id Int @id @default(autoincrement())
username String @unique
password String
email String
password String?
email String? @unique
emailVerified Boolean @default(false)
googleID String? @unique
isGuest Boolean @default(false)
partyPlayed Int @default(0)
LessonHistory LessonHistory[]
SongHistory SongHistory[]
searchHistory SearchHistory[]
settings UserSettings?
likedSongs LikedSongs[]
}
model LikedSongs {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
song Song @relation(fields: [songId], references: [id], onDelete: Cascade)
songId Int
addedDate DateTime @default(now())
}
model UserSettings {
@@ -59,6 +77,7 @@ model Song {
genre Genre? @relation(fields: [genreId], references: [id])
difficulties Json
SongHistory SongHistory[]
likedByUsers LikedSongs[]
}
model SongHistory {

View File

@@ -13,13 +13,14 @@ import {
Query,
Req,
} 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';
@Controller('album')
@ApiTags('album')
@@ -29,6 +30,7 @@ export class AlbumController {
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 +47,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,6 +57,8 @@ export class AlbumController {
}
@Get()
@ApiOkResponsePlaginated(_Album)
@ApiOperation({ description: "Get all albums paginated"})
async findAll(
@Req() req: Request,
@FilterQuery(AlbumController.filterableFields)
@@ -70,6 +75,8 @@ export class AlbumController {
}
@Get(':id')
@ApiOperation({ description: "Get an album by id"})
@ApiOkResponse({ type: _Album})
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.albumService.album({ id });

View File

@@ -1,11 +1,13 @@
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

@@ -15,14 +15,15 @@ import {
Req,
StreamableFile,
} 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';
@Controller('artist')
@ApiTags('artist')
@@ -32,6 +33,7 @@ export class ArtistController {
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 +43,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 +53,8 @@ export class ArtistController {
}
@Get(':id/illustration')
@ApiOperation({ description: "Get an artist's illustration"})
@ApiNotFoundResponse({ description: "Artist or illustration not found"})
async getIllustration(@Param('id', ParseIntPipe) id: number) {
const artist = await this.service.get({ id });
if (!artist) throw new NotFoundException('Artist not found');
@@ -66,6 +71,8 @@ export class ArtistController {
}
@Get()
@ApiOperation({ description: "Get all artists paginated"})
@ApiOkResponsePlaginated(_Artist)
async findAll(
@Req() req: Request,
@FilterQuery(ArtistController.filterableFields)
@@ -82,6 +89,8 @@ export class ArtistController {
}
@Get(':id')
@ApiOperation({ description: "Get an artist by id"})
@ApiOkResponse({ type: _Artist})
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.service.get({ id });

View File

@@ -7,11 +7,20 @@ import {
Body,
Delete,
BadRequestException,
ConflictException,
HttpCode,
Put,
InternalServerErrorException,
Patch,
NotFoundException,
Req,
UseInterceptors,
UploadedFile,
HttpStatus,
ParseFilePipeBuilder,
Response,
Query,
Param,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -19,9 +28,15 @@ import { LocalAuthGuard } from './local-auth.guard';
import { RegisterDto } from './dto/register.dto';
import { UsersService } from 'src/users/users.service';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiBody,
ApiConflictResponse,
ApiCreatedResponse,
ApiNoContentResponse,
ApiOkResponse,
ApiOperation,
ApiResponse,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
@@ -32,6 +47,10 @@ import { Profile } from './dto/profile.dto';
import { Setting } from 'src/models/setting';
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
import { SettingsService } from 'src/settings/settings.service';
import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
import { writeFile } from 'fs';
import { PasswordResetDto } from './dto/password_reset.dto ';
@ApiTags('auth')
@Controller('auth')
@@ -42,38 +61,148 @@ export class AuthController {
private settingsService: SettingsService,
) {}
@Get('login/google')
@UseGuards(AuthGuard('google'))
@ApiOperation({description: 'Redirect to google login page'})
googleLogin() {}
@Get('logged/google')
@ApiOperation({description: 'Redirect to the front page after connecting to the google account'})
@UseGuards(AuthGuard('google'))
async googleLoginCallbakc(@Req() req: any) {
let user = await this.usersService.user({ googleID: req.user.googleID });
if (!user) {
user = await this.usersService.createUser(req.user);
await this.settingsService.createUserSetting(user.id);
}
return this.authService.login(user);
}
@Post('register')
@ApiOperation({description: 'Register a new user'})
@ApiConflictResponse({ description: 'Username or email already taken' })
@ApiOkResponse({ description: 'Successfully registered, email sent to verify' })
@ApiBadRequestResponse({ description: 'Invalid data or database error' })
async register(@Body() registerDto: RegisterDto): Promise<void> {
try {
const user = await this.usersService.createUser(registerDto)
const user = await this.usersService.createUser(registerDto);
await this.settingsService.createUserSetting(user.id);
} catch(e) {
await this.authService.sendVerifyMail(user);
} catch (e) {
// check if the error is a duplicate key error
if (e.code === 'P2002') {
throw new ConflictException('Username or email already taken');
}
console.error(e);
throw new BadRequestException();
}
}
@Put('verify')
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@ApiOperation({description: 'Verify the email of the user'})
@ApiOkResponse({ description: 'Successfully verified' })
@ApiBadRequestResponse({ description: 'Invalid or expired token' })
async verify(@Request() req: any, @Query('token') token: string): Promise<void> {
if (await this.authService.verifyMail(req.user.id, token))
return;
throw new BadRequestException("Invalid token. Expired or invalid.");
}
@Put('reverify')
@UseGuards(JwtAuthGuard)
@HttpCode(200)
@ApiOperation({description: 'Resend the verification email'})
async reverify(@Request() req: any): Promise<void> {
const user = await this.usersService.user({ id: req.user.id });
if (!user) throw new BadRequestException('Invalid user');
await this.authService.sendVerifyMail(user);
}
@HttpCode(200)
@Put('password-reset')
async password_reset(
@Body() resetDto: PasswordResetDto,
@Query('token') token: string,
): Promise<void> {
if (await this.authService.changePassword(resetDto.password, token)) return;
throw new BadRequestException('Invalid token. Expired or invalid.');
}
@HttpCode(200)
@Put('forgot-password')
async forgot_password(@Query('email') email: string): Promise<void> {
console.log(email);
const user = await this.usersService.user({ email: email });
if (!user) throw new BadRequestException('Invalid user');
await this.authService.sendPasswordResetMail(user);
}
@Post('login')
@ApiBody({ type: LoginDto })
@HttpCode(200)
@UseGuards(LocalAuthGuard)
@Post('login')
@ApiBody({ type: LoginDto })
@ApiOperation({ description: 'Login with username and password' })
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
@ApiUnauthorizedResponse({ description: 'Invalid credentials' })
async login(@Request() req: any): Promise<JwtToken> {
return this.authService.login(req.user);
}
@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);
return this.authService.login(user);
}
@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')
async getProfilePicture(@Request() req: any, @Response() res: any) {
return await this.usersService.getProfilePicture(req.user.id, res);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'The user profile picture' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Post('me/picture')
@ApiOperation({ description: 'Upload a new profile picture' })
@UseInterceptors(FileInterceptor('file'))
async postProfilePicture(
@Request() req: any,
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: 'jpeg',
})
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
}),
)
file: Express.Multer.File,
) {
const path = `/data/${req.user.id}.jpg`;
writeFile(path, file.buffer, (err) => {
if (err) throw err;
});
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully logged in', type: User })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me')
@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();
@@ -85,6 +214,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>,
@@ -110,32 +240,79 @@ 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 });
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
@ApiUnauthorizedResponse({description: 'Invalid token'})
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Patch('me/settings')
@ApiOperation({ description: 'Edit the settings of connected user' })
udpateSettings(
@Request() req: any,
@Body() settingUserDto: UpdateSettingDto): Promise<Setting> {
@Body() settingUserDto: UpdateSettingDto,
): Promise<Setting> {
return this.settingsService.updateUserSettings({
where: { userId: +req.user.id},
where: { userId: +req.user.id },
data: settingUserDto,
});
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
@ApiUnauthorizedResponse({description: 'Invalid token'})
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me/settings')
@ApiOperation({ description: 'Get the settings of connected user' })
async getSettings(@Request() req: any): Promise<Setting> {
const result = await this.settingsService.getUserSetting({ userId: +req.user.id });
const result = await this.settingsService.getUserSetting({
userId: +req.user.id,
});
if (!result) throw new NotFoundException();
return result;
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully added liked song'})
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Post('me/likes/:id')
addLikedSong(
@Request() req: any,
@Param('id') 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

@@ -9,6 +9,7 @@ import { ConfigModule } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
import { SettingsModule } from 'src/settings/settings.module';
import { GoogleStrategy } from './google.strategy';
@Module({
imports: [
@@ -20,12 +21,12 @@ import { SettingsModule } from 'src/settings/settings.module';
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
signOptions: { expiresIn: '365d' },
}),
inject: [ConfigService],
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy],
controllers: [AuthController],
})
export class AuthModule {}

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(
@@ -15,7 +18,7 @@ export class AuthService {
password: string,
): Promise<PayloadInterface | null> {
const user = await this.userService.user({ username });
if (user && bcrypt.compareSync(password, user.password)) {
if (user && user.password && bcrypt.compareSync(password, user.password)) {
return {
username: user.username,
id: user.id,
@@ -31,4 +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

@@ -0,0 +1,35 @@
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['email', 'profile'],
});
}
async validate(
_accessToken: string,
_refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const user = {
email: profile.emails[0].value,
username: profile.displayName,
password: null,
googleID: profile.id,
// firstName: name.givenName,
// lastName: name.familyName,
// picture: photos[0].value,
};
done(null, user);
return user;
}
}

View File

@@ -14,7 +14,7 @@ import {
Req,
StreamableFile,
} 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,6 +22,7 @@ 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';
@Controller('genre')
@ApiTags('genre')
@@ -65,6 +66,7 @@ export class GenreController {
}
@Get()
@ApiOkResponsePlaginated(_Genre)
async findAll(
@Req() req: Request,
@FilterQuery(GenreController.filterableFields)

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,25 @@ 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 +40,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 +54,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

@@ -13,11 +13,12 @@ import {
Delete,
NotFoundException,
} 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';
export class Lesson {
@ApiProperty()
@@ -48,6 +49,7 @@ export class LessonController {
summary: 'Get all lessons',
})
@Get()
@ApiOkResponsePlaginated(_Lesson)
async getAll(
@Req() request: Request,
@FilterQuery(LessonController.filterableFields)

View File

@@ -1,24 +1,71 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaService } from './prisma/prisma.service';
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,
}),
),
),
);
}
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app);
app.use(
RequestLogger.buildExpressRequestLogger({
doNotLogPaths: ['/health'],
} as RequestLoggerOptions),
);
app.enableShutdownHooks();
const config = new DocumentBuilder()
.setTitle('Chromacase')
.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,21 @@
* 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 +54,23 @@ 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,4 +1,4 @@
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
@@ -6,10 +6,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

View File

@@ -12,20 +12,29 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { ApiOkResponse, ApiOperation, ApiParam, 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';
@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,
@Param('query') query: string,
): Promise<Song[] | null> {
try {
const ret = await this.searchService.songByGuess(query, req.user?.id);
if (!ret.length) throw new NotFoundException();
@@ -37,6 +46,9 @@ export class SearchController {
@Get('genres/:query')
@UseGuards(JwtAuthGuard)
@ApiUnauthorizedResponse({ description: "Invalid token"})
@ApiOkResponse({ type: _Genre, isArray: true})
@ApiOperation({ description: "Search a genre"})
async searchGenre(@Request() req: any, @Param('query') query: string): Promise<Genre[] | null> {
try {
const ret = await this.searchService.genreByGuess(query, req.user?.id);
@@ -49,6 +61,9 @@ export class SearchController {
@Get('artists/:query')
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _Artist, isArray: true})
@ApiUnauthorizedResponse({ description: "Invalid token"})
@ApiOperation({ description: "Search an artist"})
async searchArtists(@Request() req: any, @Param('query') query: string): Promise<Artist[] | null> {
try {
const ret = await this.searchService.artistByGuess(query, req.user?.id);

View File

@@ -5,7 +5,10 @@ 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[]> {
return this.prisma.song.findMany({

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,16 +16,26 @@ 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, ApiResponse, ApiResponseProperty, 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';
class SongHistoryResult {
@ApiProperty()
best: number;
@ApiProperty({ type: SongHistory, isArray: true})
history: SongHistory[];
}
@Controller('song')
@ApiTags('song')
@@ -44,6 +54,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 +70,9 @@ 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"})
async getIllustration(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException('Song not found');
@@ -74,6 +90,9 @@ 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 +102,7 @@ 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({
@@ -98,6 +118,7 @@ export class SongController {
: undefined,
});
} catch {
throw new ConflictException(
await this.songService.song({ name: createSongDto.name }),
);
@@ -105,6 +126,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,6 +136,7 @@ export class SongController {
}
@Get()
@ApiOkResponsePlaginated(_Song)
async findAll(
@Req() req: Request,
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
@@ -129,6 +152,9 @@ export class SongController {
}
@Get(':id')
@ApiOperation({ description: "Get a specific song data"})
@ApiNotFoundResponse({ description: "Song not found"})
@ApiOkResponse({ type: _Song, description: "Requested song"})
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.songService.song({ id });
@@ -139,6 +165,8 @@ 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({
@@ -146,10 +174,4 @@ export class SongController {
songId: id,
});
}
@Get('/artist/:artistId')
async getSongByArtist(@Param('artistId', ParseIntPipe) artistId: number) {
const res = await this.songService.songByArtist(artistId)
return res;
}
}

View File

@@ -1,6 +1,6 @@
import { Controller, Get, Param, NotFoundException } 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')
@@ -20,4 +20,10 @@ export class UsersController {
if (!ret) throw new NotFoundException();
return ret;
}
@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

@@ -1,12 +1,20 @@
import { Injectable } from '@nestjs/common';
import {
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import * as bcrypt from 'bcryptjs';
import { randomUUID } from 'crypto';
import { createHash, randomUUID } from 'crypto';
import { createReadStream, existsSync } from 'fs';
import fetch from 'node-fetch';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
@@ -34,7 +42,7 @@ export class UsersService {
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
data.password = await bcrypt.hash(data.password, 8);
if (data.password) data.password = await bcrypt.hash(data.password, 8);
return this.prisma.user.create({
data,
});
@@ -46,7 +54,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: '',
},
});
@@ -72,4 +80,56 @@ export class UsersService {
where,
});
}
async getProfilePicture(userId: number, res: any) {
const path = `/data/${userId}.jpg`;
if (existsSync(path)) {
const file = createReadStream(path);
return file.pipe(res);
}
// 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);
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 },
}
)
}
}

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

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

@@ -11,10 +11,12 @@
"dependencies": {
"crawlee": "^3.0.0",
"fs": "^0.0.1-security",
"playwright": "^1.28.0"
"playwright": "^1.28.0",
"slug": "^8.2.3"
},
"devDependencies": {
"@apify/tsconfig": "^0.1.0",
"@types/slug": "^5.0.5",
"ts-node": "^10.8.0",
"typescript": "^4.7.4"
}
@@ -778,6 +780,12 @@
"@types/node": "*"
}
},
"node_modules/@types/slug": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@types/slug/-/slug-5.0.5.tgz",
"integrity": "sha512-vcHM79Xu5ALOC90kf5S1B4XGbRl8VW6f1+6jpBmK/FLHi4AyWKAVENgMOyHFyjHV5vDbNRPtjsNJuPRqrLBOxw==",
"dev": true
},
"node_modules/@types/tough-cookie": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@@ -2760,6 +2768,14 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/slug": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/slug/-/slug-8.2.3.tgz",
"integrity": "sha512-fXjhAZszNecz855GUNIwW0+sFPi9WV4bMiEKDOCA4wcq1ts1UnUVNy/F78B0Aat7/W3rA+se//33ILKNMrbeYQ==",
"bin": {
"slug": "cli.js"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -3848,6 +3864,12 @@
"@types/node": "*"
}
},
"@types/slug": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@types/slug/-/slug-5.0.5.tgz",
"integrity": "sha512-vcHM79Xu5ALOC90kf5S1B4XGbRl8VW6f1+6jpBmK/FLHi4AyWKAVENgMOyHFyjHV5vDbNRPtjsNJuPRqrLBOxw==",
"dev": true
},
"@types/tough-cookie": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@@ -5233,6 +5255,11 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"slug": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/slug/-/slug-8.2.3.tgz",
"integrity": "sha512-fXjhAZszNecz855GUNIwW0+sFPi9WV4bMiEKDOCA4wcq1ts1UnUVNy/F78B0Aat7/W3rA+se//33ILKNMrbeYQ=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@@ -6,10 +6,12 @@
"dependencies": {
"crawlee": "^3.0.0",
"fs": "^0.0.1-security",
"playwright": "^1.28.0"
"playwright": "^1.28.0",
"slug": "^8.2.3"
},
"devDependencies": {
"@apify/tsconfig": "^0.1.0",
"@types/slug": "^5.0.5",
"ts-node": "^10.8.0",
"typescript": "^4.7.4"
},

View File

@@ -17,5 +17,5 @@ const crawler = new PlaywrightCrawler({
// Add first URL to the queue and start the crawl.
await crawler.run([
"https://musescore.com/sheetmusic?complexity=1&instrument=2&license=to_modify_commercially%2Cto_use_commercially&recording_type=public-domain",
"https://musescore.com/sheetmusic?complexity=1&instrument=2&instrumentation=114&license=to_modify_commercially%2Cto_use_commercially&recording_type=public-domain&sort=rating",
]);

View File

@@ -2,6 +2,7 @@ import { Dataset, createPlaywrightRouter } from "crawlee";
import * as fs from "fs";
import { sleep } from "crawlee";
export const router = createPlaywrightRouter();
import slug from "slug";
router.addDefaultHandler(async ({ enqueueLinks }) => {
const songs = await enqueueLinks({
@@ -18,13 +19,17 @@ router.addDefaultHandler(async ({ enqueueLinks }) => {
router.addHandler("SONG", async ({ request, page }) => {
await Dataset.pushData({ url: request.loadedUrl });
await page.waitForSelector('aside div div section button[name="download"]');
const title = await page.locator("h1").textContent();
const artist = await page
let og_title = await page.locator("h1").textContent();
if (og_title == null) return
let title = slug(og_title);
let artist = await page
.locator(
"body > div.js-page.react-container > div > section > aside > div:nth-child(5) > div > section > h3:nth-child(2) > a"
)
.first()
.textContent();
if (artist == null) return
artist = slug(artist);
const genres = await page
.locator(
"body > div.js-page.react-container > div > section > aside > div:nth-child(6) > div > table > tbody > tr:nth-child(5) > td > div > a"
@@ -66,7 +71,7 @@ router.addHandler("SONG", async ({ request, page }) => {
`../musics/a/${title}/${title}.ini`,
`
[Metadata]
Name=${title}
Name=${og_title}
Artist=${artist}
Genre=${genres}
Album=

1
data/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*

View File

@@ -1,3 +1,10 @@
networks:
loki:
volumes:
scoro_logs:
services:
back:
build:
@@ -9,6 +16,7 @@ services:
volumes:
- ./back:/app
- ./assets:/assets
- ./data:/data
depends_on:
db:
condition: service_healthy
@@ -24,6 +32,9 @@ services:
volumes:
- ./scorometer:/app
- ./assets:/assets
- scoro_logs:/logs
networks:
- loki
db:
container_name: db
@@ -39,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:
@@ -54,3 +66,19 @@ services:
- "back"
env_file:
- .env
nginx:
image: nginx
environment:
- API_URL=http://back:3000
- SCOROMETER_URL=http://scorometer:6543
- FRONT_URL=http://front:19006
- PORT=4567
depends_on:
- back
- front
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
@@ -10,18 +16,20 @@ services:
- .env
volumes:
- ./assets:/assets
- ./data:/data
scorometer:
image: ghcr.io/chroma-case/scorometer:main
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"

View File

@@ -1,3 +1,12 @@
networks:
loki:
volumes:
db:
scoro_logs:
services:
back:
build: ./back
@@ -10,12 +19,14 @@ services:
- .env
volumes:
- ./assets:/assets
- ./data:/data
scorometer:
build: ./scorometer
ports:
- "6543:6543"
volumes:
- ./assets:/assets
- scoro_logs:/logs
db:
container_name: db
image: postgres:alpine3.14
@@ -34,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,6 +56,3 @@ services:
- "back"
env_file:
- .env
volumes:
db:

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

@@ -2,3 +2,6 @@ node_modules/
.expo/
.idea/
.vscode/
.dockerignore
Dockerfile
Dockerfile.dev

2
front/.gitignore vendored
View File

@@ -14,5 +14,7 @@ yarn.error*
# macOS
.DS_Store
yarn-error.log
.idea/
.expo

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,6 +21,8 @@ import { PlageHandler } from './models/Plage';
import { ListHandler } from './models/List';
import { AccessTokenResponseHandler } from './models/AccessTokenResponse';
import * as yup from 'yup';
import { base64ToBlob } from './utils/base64ToBlob';
import { ImagePickerAsset } from 'expo-image-picker';
type AuthenticationInput = { username: string; password: string };
type RegistrationInput = AuthenticationInput & { email: string };
@@ -30,6 +32,7 @@ export type AccessToken = string;
type FetchParams = {
route: string;
body?: object;
formData?: FormData;
method?: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT';
};
@@ -65,7 +68,7 @@ export default class API {
public static readonly baseUrl =
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
? '/api'
: Constants.manifest?.extra?.apiUrl;
: 'https://nightly.chroma.octohub.app/api';
public static async fetch(
params: FetchParams,
handle: Pick<Required<HandleParams>, 'raw'>
@@ -81,17 +84,30 @@ export default class API {
public static async fetch(params: FetchParams): Promise<void>;
public static async fetch(params: FetchParams, handle?: HandleParams) {
const jwtToken = store.getState().user.accessToken;
const header = {
'Content-Type': 'application/json',
const headers = {
...(params.formData == undefined && { 'Content-Type': 'application/json' }),
...(jwtToken && { Authorization: `Bearer ${jwtToken}` }),
};
const response = await fetch(`${API.baseUrl}${params.route}`, {
headers: (jwtToken && { ...header, Authorization: `Bearer ${jwtToken}` }) || header,
body: JSON.stringify(params.body),
headers: headers,
body: params.formData ?? JSON.stringify(params.body),
method: params.method ?? 'GET',
}).catch(() => {
throw new Error('Error while fetching API: ' + API.baseUrl);
});
if (!handle || handle.emptyResponse) {
if (!response.ok) {
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;
}
if (handle.raw) {
@@ -102,7 +118,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) {
@@ -164,6 +180,7 @@ export default class API {
{
route: '/auth/guest',
method: 'POST',
body: undefined,
},
{ handler: AccessTokenResponseHandler }
)
@@ -297,6 +314,24 @@ export default class API {
};
}
/**
* 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
@@ -332,15 +367,22 @@ export default class API {
return `${API.baseUrl}/genre/${genreId}/illustration`;
}
// public static getGenre(genreId: number): Query<Genre> {
// return {
// key: ['genre', genreId],
// exec: () =>
// API.fetch({
// route: `/genre/${genreId}`,
// }),
// }
// }
/**
* 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
@@ -508,16 +550,6 @@ export default class API {
};
}
// public static getFavorites(): Query<Song[]> {
// return {
// key: 'favorites',
// exec: () =>
// API.fetch({
// route: '/search/songs/o',
// }),
// };
// }
/**
* Retrieve the authenticated user's search history
* @param skip number of entries skipped before returning
@@ -626,4 +658,43 @@ export default class API {
{ handler: UserHandler }
);
}
public static async updateProfileAvatar(image: ImagePickerAsset): Promise<void> {
const data = await base64ToBlob(image.uri);
const formData = new FormData();
formData.append('file', data);
return API.fetch({
route: '/auth/me/picture',
method: 'POST',
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

@@ -22,6 +22,5 @@ RUN yarn tsc && expo build: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';
@@ -29,6 +28,13 @@ 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,
@@ -61,7 +72,7 @@ const protectedRoutes = () =>
},
Genre: {
component: GenreDetailsView,
options: { title: translate('genreFilter')},
options: { title: translate('genreFilter') },
link: '/genre/:genreId',
},
Score: {
@@ -80,6 +91,11 @@ const protectedRoutes = () =>
link: undefined,
},
User: { component: ProfileView, options: { title: translate('user') }, link: '/user' },
Verified: {
component: VerifiedView,
options: { title: 'Verify email', headerShown: false },
link: '/verify',
},
} as const);
const publicRoutes = () =>
@@ -90,15 +106,13 @@ 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: {
@@ -106,6 +120,21 @@ const publicRoutes = () =>
options: { title: 'Oops', headerShown: false },
link: undefined,
},
Google: {
component: GoogleView,
options: { title: 'Google signin', headerShown: false },
link: '/logged/google',
},
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

View File

@@ -4,77 +4,126 @@ import { useEffect } from 'react';
const ThemeProvider = ({ children }: { children: JSX.Element }) => {
const colorScheme = useColorScheme();
const config = {
dependencies: {
"linear-gradient": require("expo-linear-gradient").LinearGradient,
},
};
return (
<NativeBaseProvider
config={config}
theme={extendTheme({
config: {
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',
},
},
}),
},
},
})}

View File

@@ -6,7 +6,7 @@ module.exports = {
icon: './assets/icon.png',
userInterfaceStyle: 'light',
splash: {
image: './assets/splashLogo.png',
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
@@ -18,12 +18,6 @@ module.exports = {
supportsTablet: true,
},
android: {
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#FFFFFF',
package: 'com.chromacase.chromacase',
versionCode: 1,
},
package: 'build.apk',
},
web: {

View File

@@ -7,7 +7,7 @@
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splashLogo.png",
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#ffffff"
},
@@ -19,10 +19,6 @@
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "build.apk"
},
"web": {
@@ -32,6 +28,14 @@
"eas": {
"projectId": "dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2"
}
}
},
"plugins": [
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let you set your personal avatar."
}
]
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

BIN
front/assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

BIN
front/assets/full_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
front/assets/full_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

BIN
front/assets/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 65 KiB

BIN
front/assets/icon_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

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