Compare commits
164 Commits
feature/ad
...
feat/crawl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfd4a8acec | ||
|
|
e63789cbc1 | ||
|
|
4a4f9e2a55 | ||
| 3860c9f72a | |||
| b02b23a978 | |||
| 5b0c1f8992 | |||
|
|
8155549031 | ||
|
|
1ca4633360 | ||
|
|
bb304fa8cd | ||
|
|
9a1f1f78cb | ||
|
|
96bb830600 | ||
|
|
1333b74001 | ||
|
|
ece87dbdb9 | ||
|
|
e82a6b1dd6 | ||
|
|
cd2e119dc6 | ||
|
|
c9928f1cce | ||
|
|
7aac3922d6 | ||
|
|
82403c811e | ||
|
|
230c60bcd0 | ||
|
|
177e903b07 | ||
|
|
a11c236753 | ||
|
|
29ef585410 | ||
|
|
f8be2c2462 | ||
|
|
7d27af1e2d | ||
|
|
258fe91ae7 | ||
|
|
711b5d583b | ||
|
|
4416808056 | ||
|
|
979c27c087 | ||
|
|
b3117886cf | ||
|
|
1c248fa479 | ||
|
|
ec62f4b085 | ||
|
|
04bad30aaa | ||
|
|
e5a52d0f94 | ||
|
|
68c6c6fa11 | ||
|
|
94a64d16e6 | ||
| 7aa7f50ecb | |||
| ee8e0e26db | |||
| 31b965e8f6 | |||
| 94658d4379 | |||
|
|
49a735631a | ||
|
|
1905daec60 | ||
|
|
7a1f4fb787 | ||
|
|
f3cdba34fb | ||
|
|
5b7cb6746d | ||
|
|
6e3e73982f | ||
|
|
8e5c65e6f2 | ||
|
|
94875d4c7f | ||
|
|
e817021ede | ||
|
|
dcca1b1f1c | ||
|
|
c0c2918e72 | ||
|
|
973f9bf5b3 | ||
|
|
162fc9148f | ||
|
|
57d646f6eb | ||
|
|
6768b0b2a6 | ||
|
|
fa14d1f979 | ||
|
|
c4ca2e509e | ||
|
|
1abfbf391f | ||
|
|
073ff033f3 | ||
|
|
23e5941700 | ||
|
|
027d450579 | ||
|
|
ad9bbbc2b9 | ||
|
|
58af78b1d3 | ||
|
|
09d2da8eec | ||
|
|
8abaaf6624 | ||
|
|
3c3697be61 | ||
|
|
073c00a35e | ||
|
|
58d761c359 | ||
|
|
aaaf73f632 | ||
|
|
f83043a9c9 | ||
|
|
cea6d8d0bc | ||
|
|
607c35b621 | ||
|
|
13d0be4586 | ||
|
|
3e1e41f117 | ||
|
|
8f9d7e4a85 | ||
|
|
1e504c8982 | ||
|
|
e56436db3a | ||
|
|
bc227fb0ea | ||
|
|
49bc4f9f45 | ||
|
|
73076c4b28 | ||
|
|
8732972b3f | ||
|
|
cd9d64e501 | ||
|
|
62bf7ec035 | ||
|
|
659f5d5d84 | ||
|
|
bbc53f04de | ||
|
|
431427d7ad | ||
|
|
611ab57c5d | ||
| bc13c10f1a | |||
| 91c9e2b295 | |||
| 585be2aa19 | |||
| 654022b48a | |||
| afab03baf8 | |||
| a52c10fc2c | |||
| f2ed598865 | |||
| 02fc8175f4 | |||
|
|
628e50a48d | ||
|
|
70ab56ce3a | ||
|
|
1fefe7912d | ||
|
|
c21f5f0659 | ||
|
|
46ef0a7f1b | ||
|
|
b43c64962a | ||
|
|
64640eda55 | ||
|
|
a6d9cb3b40 | ||
|
|
b61541f7b8 | ||
|
|
3ff523560b | ||
|
|
b61968706d | ||
|
|
2f27278d3a | ||
|
|
e1ab9fe118 | ||
|
|
b1d0415ba0 | ||
|
|
8ab85ab689 | ||
|
|
16cd794e3b | ||
|
|
f85c30a53b | ||
|
|
6da96ed886 | ||
|
|
852fbd5c87 | ||
|
|
5cec62d1b1 | ||
|
|
7e866f9826 | ||
|
|
2f50f694f3 | ||
|
|
e0f2674811 | ||
|
|
b84ee11f45 | ||
|
|
a2494ce498 | ||
|
|
b76d496034 | ||
|
|
a81d3ee34d | ||
|
|
85473ae492 | ||
|
|
9655e986ff | ||
|
|
101ea8498b | ||
|
|
7d33f85cbc | ||
|
|
66d792715e | ||
|
|
40581f4a45 | ||
|
|
2ca3fcb81a | ||
|
|
30fcacbec6 | ||
|
|
7c3289ccec | ||
|
|
7438986bcd | ||
|
|
3ac017a5f0 | ||
|
|
8e5cc1bc44 | ||
|
|
125a7faf02 | ||
|
|
c9d3ef88e7 | ||
|
|
0ba3bec5aa | ||
|
|
539c35c903 | ||
|
|
e1463d41b9 | ||
|
|
c81f8df61c | ||
|
|
a3676fabf8 | ||
|
|
dc398d6e06 | ||
|
|
d5da112a01 | ||
|
|
96048bd671 | ||
|
|
dcdc6b196d | ||
|
|
9f542fc9dd | ||
| 930191569f | |||
| 74cd9c0df2 | |||
| d2642b4fb8 | |||
| ebcc48cc57 | |||
| 95b08935cc | |||
| 04487c9b24 | |||
|
|
20eb62d19b | ||
|
|
567d3250e2 | ||
| 4207d5ee50 | |||
|
|
509cc5b9f8 | ||
|
|
1b22dba9cd | ||
|
|
2ec95dd3c3 | ||
|
|
c0d9ee7ca6 | ||
|
|
27f7945289 | ||
|
|
5a190f3b96 | ||
|
|
3d76834f45 | ||
| ccc86895e2 | |||
| 279d16d59a | |||
| 04d288b844 |
@@ -8,6 +8,6 @@ insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = tab
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
[{*.yaml,*.yml,*.nix}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
14
.github/workflows/CI.yml
vendored
@@ -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
@@ -13,3 +13,6 @@ log.html
|
||||
node_modules/
|
||||
./front/coverage
|
||||
.venv
|
||||
.data
|
||||
.DS_Store
|
||||
_gen
|
||||
|
||||
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 932 B |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,4 +1,4 @@
|
||||
#!/bin/env python3
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -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
@@ -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": [
|
||||
|
||||
12
back/prisma/migrations/20230621090510_google/migration.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[googleID]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "googleID" TEXT,
|
||||
ALTER COLUMN "password" DROP NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_googleID_key" ON "User"("googleID");
|
||||
2
back/prisma/migrations/20230907141258_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "LikedSongs" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"songId" INTEGER NOT NULL,
|
||||
"addedDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "LikedSongs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LikedSongs" ADD CONSTRAINT "LikedSongs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LikedSongs" ADD CONSTRAINT "LikedSongs_songId_fkey" FOREIGN KEY ("songId") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
8
back/prisma/migrations/20230920151856_/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
2
back/prisma/migrations/20230921103156_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
8
back/src/auth/dto/password_reset.dto .ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class PasswordResetDto {
|
||||
@ApiProperty()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
35
back/src/auth/google.strategy.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
scope: ['email', 'profile'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
_accessToken: string,
|
||||
_refreshToken: string,
|
||||
profile: any,
|
||||
done: VerifyCallback,
|
||||
): Promise<any> {
|
||||
const user = {
|
||||
email: profile.emails[0].value,
|
||||
username: profile.displayName,
|
||||
password: null,
|
||||
googleID: profile.id,
|
||||
// firstName: name.givenName,
|
||||
// lastName: name.familyName,
|
||||
// picture: photos[0].value,
|
||||
};
|
||||
done(null, user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -6,7 +6,7 @@ export class User {
|
||||
@ApiProperty()
|
||||
username: string;
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
email: string | null;
|
||||
@ApiProperty()
|
||||
isGuest: boolean;
|
||||
@ApiProperty()
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
22
config/promtail-local-config.yaml
Normal 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'
|
||||
|
||||
29
crawler/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
*
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
31
flake.nix
@@ -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
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2,3 +2,6 @@ node_modules/
|
||||
.expo/
|
||||
.idea/
|
||||
.vscode/
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
||||
|
||||
2
front/.gitignore
vendored
@@ -14,5 +14,7 @@ yarn.error*
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
yarn-error.log
|
||||
|
||||
.idea/
|
||||
.expo
|
||||
123
front/API.ts
@@ -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) }
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
149
front/Theme.tsx
@@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
})}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB |
BIN
front/assets/auth/guest_banner.png
Normal file
|
After Width: | Height: | Size: 657 KiB |
BIN
front/assets/auth/login_banner.png
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
front/assets/auth/register_banner.png
Normal file
|
After Width: | Height: | Size: 609 KiB |
BIN
front/assets/banner.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 18 KiB |
BIN
front/assets/fonts/Lexend-VariableFont_wght.ttf
Normal file
BIN
front/assets/fonts/lexend.ttf
Normal file
BIN
front/assets/full_dark.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
front/assets/full_light.png
Normal file
|
After Width: | Height: | Size: 631 KiB |
BIN
front/assets/icon.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 65 KiB |
BIN
front/assets/icon_dark.png
Normal file
|
After Width: | Height: | Size: 96 KiB |