Merge remote-tracking branch 'origin/main' into feat/adc/search-view-v2
This commit is contained in:
@@ -16,3 +16,7 @@ GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
|
||||
SMTP_TRANSPORT=smtps://toto:tata@relay
|
||||
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
||||
IGNORE_MAILS=true
|
||||
API_KEYS=SCOROTEST,ROBOTO,SCORO
|
||||
API_KEY_SCORO_TEST=SCOROTEST
|
||||
API_KEY_ROBOT=ROBOTO
|
||||
API_KEY_SCORO=SCORO
|
||||
1
.git-blame-ignore-revs
Normal file
1
.git-blame-ignore-revs
Normal file
@@ -0,0 +1 @@
|
||||
14e241db37c4080bc0bd87363cf7a57ef8379f46
|
||||
3
.github/workflows/CI.yml
vendored
3
.github/workflows/CI.yml
vendored
@@ -119,10 +119,13 @@ jobs:
|
||||
- name: Run scorometer tests
|
||||
run: |
|
||||
pip install -r scorometer/requirements.txt
|
||||
export API_KEY_SCORO_TEST=SCOROTEST
|
||||
export API_KEY_SCORO=SCORO
|
||||
cd scorometer/tests && ./runner.sh
|
||||
|
||||
- name: Run robot tests
|
||||
run: |
|
||||
export API_KEY_ROBOT=ROBOTO
|
||||
pip install -r back/test/robot/requirements.txt
|
||||
robot -d out back/test/robot/
|
||||
- uses: actions/upload-artifact@v3
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ node_modules/
|
||||
.data
|
||||
.DS_Store
|
||||
_gen
|
||||
venv
|
||||
|
||||
@@ -8,6 +8,10 @@ from mido import MidiFile
|
||||
from configparser import ConfigParser
|
||||
|
||||
url = os.environ.get("API_URL")
|
||||
api_key = os.environ.get("API_KEY_POPULATE")
|
||||
auth_headers = {}
|
||||
auth_headers["Authorization"] = f"API Key {api_key}"
|
||||
|
||||
|
||||
def getOrCreateAlbum(name, artistId):
|
||||
if not name:
|
||||
@@ -15,7 +19,7 @@ def getOrCreateAlbum(name, artistId):
|
||||
res = requests.post(f"{url}/album", json={
|
||||
"name": name,
|
||||
"artist": artistId,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
return out["id"]
|
||||
@@ -25,7 +29,7 @@ def getOrCreateGenre(names):
|
||||
for name in names.split(","):
|
||||
res = requests.post(f"{url}/genre", json={
|
||||
"name": name,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
ids += [out["id"]]
|
||||
@@ -35,7 +39,7 @@ def getOrCreateGenre(names):
|
||||
def getOrCreateArtist(name):
|
||||
res = requests.post(f"{url}/artist", json={
|
||||
"name": name,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
return out["id"]
|
||||
@@ -49,6 +53,7 @@ def populateFile(path, midi, mxl):
|
||||
difficulties["length"] = round((mid.length), 2)
|
||||
artistId = getOrCreateArtist(metadata["Artist"])
|
||||
print(f"Populating {metadata['Name']}")
|
||||
print(auth_headers)
|
||||
res = requests.post(f"{url}/song", json={
|
||||
"name": metadata["Name"],
|
||||
"midiPath": f"/assets/{midi}",
|
||||
@@ -58,9 +63,10 @@ def populateFile(path, midi, mxl):
|
||||
"album": getOrCreateAlbum(metadata["Album"], artistId),
|
||||
"genre": getOrCreateGenre(metadata["Genre"]),
|
||||
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
|
||||
})
|
||||
}, headers=auth_headers)
|
||||
print(res.json())
|
||||
|
||||
|
||||
def main():
|
||||
global url
|
||||
if url == None:
|
||||
|
||||
2
assets/requirements.txt
Normal file
2
assets/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
mido
|
||||
requests
|
||||
11413
back/package-lock.json
generated
11413
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@
|
||||
"nodemailer": "^6.9.5",
|
||||
"opensheetmusicdisplay": "^1.8.4",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"prisma-class-generator": "^0.2.7",
|
||||
|
||||
@@ -12,23 +12,24 @@ import {
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||
import { AlbumService } from './album.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Album } from '@prisma/client';
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Album as _Album } from 'src/_gen/prisma-class/album';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateAlbumDto } from "./dto/create-album.dto";
|
||||
import { AlbumService } from "./album.service";
|
||||
import { Request } from "express";
|
||||
import { Prisma, Album } from "@prisma/client";
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Album as _Album } from "src/_gen/prisma-class/album";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
@Controller('album')
|
||||
@ApiTags('album')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller("album")
|
||||
@ApiTags("album")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class AlbumController {
|
||||
static filterableFields: string[] = ['+id', 'name', '+artistId'];
|
||||
static filterableFields: string[] = ["+id", "name", "+artistId"];
|
||||
static includableFields: IncludeMap<Prisma.AlbumInclude> = {
|
||||
artist: true,
|
||||
Song: true,
|
||||
@@ -38,7 +39,7 @@ export class AlbumController {
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
description: 'Register a new album, should not be used by frontend',
|
||||
description: "Register a new album, should not be used by frontend",
|
||||
})
|
||||
async create(@Body() createAlbumDto: CreateAlbumDto) {
|
||||
try {
|
||||
@@ -55,26 +56,26 @@ export class AlbumController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: 'Delete an album by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "Delete an album by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.albumService.deleteAlbum({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponsePlaginated(_Album)
|
||||
@ApiOperation({ description: 'Get all albums paginated' })
|
||||
@ApiOperation({ description: "Get all albums paginated" })
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(AlbumController.filterableFields)
|
||||
where: Prisma.AlbumWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Album>> {
|
||||
const ret = await this.albumService.albums({
|
||||
skip,
|
||||
@@ -85,20 +86,20 @@ export class AlbumController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: 'Get an album by id' })
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get an album by id" })
|
||||
@ApiOkResponse({ type: _Album })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.albumService.album(
|
||||
{ id },
|
||||
mapInclude(include, req, AlbumController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Album not found');
|
||||
if (res === null) throw new NotFoundException("Album not found");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { AlbumController } from './album.controller';
|
||||
import { AlbumService } from './album.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { AlbumController } from "./album.controller";
|
||||
import { AlbumService } from "./album.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Album } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Album } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class AlbumService {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateAlbumDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
|
||||
describe('AppController', () => {
|
||||
describe("AppController", () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -14,9 +14,9 @@ describe('AppController', () => {
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
describe("root", () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
expect(appController.getHello()).toBe("Hello World!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { ApiOkResponse } from '@nestjs/swagger';
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { AppService } from "./app.service";
|
||||
import { ApiOkResponse } from "@nestjs/swagger";
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@@ -8,7 +8,7 @@ export class AppController {
|
||||
|
||||
@Get()
|
||||
@ApiOkResponse({
|
||||
description: 'Return a hello world message, used as a health route',
|
||||
description: "Return a hello world message, used as a health route",
|
||||
})
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PrismaService } from './prisma/prisma.service';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { SongModule } from './song/song.module';
|
||||
import { LessonModule } from './lesson/lesson.module';
|
||||
import { SettingsModule } from './settings/settings.module';
|
||||
import { ArtistService } from './artist/artist.service';
|
||||
import { GenreModule } from './genre/genre.module';
|
||||
import { ArtistModule } from './artist/artist.module';
|
||||
import { AlbumModule } from './album/album.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
import { HistoryModule } from './history/history.module';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { ScoresModule } from './scores/scores.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
import { PrismaService } from "./prisma/prisma.service";
|
||||
import { UsersModule } from "./users/users.module";
|
||||
import { PrismaModule } from "./prisma/prisma.module";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { SongModule } from "./song/song.module";
|
||||
import { LessonModule } from "./lesson/lesson.module";
|
||||
import { SettingsModule } from "./settings/settings.module";
|
||||
import { ArtistService } from "./artist/artist.service";
|
||||
import { GenreModule } from "./genre/genre.module";
|
||||
import { ArtistModule } from "./artist/artist.module";
|
||||
import { AlbumModule } from "./album/album.module";
|
||||
import { SearchModule } from "./search/search.module";
|
||||
import { HistoryModule } from "./history/history.module";
|
||||
import { MailerModule } from "@nestjs-modules/mailer";
|
||||
import { ScoresModule } from "./scores/scores.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -30,7 +30,7 @@ import { ScoresModule } from './scores/scores.module';
|
||||
SearchModule,
|
||||
SettingsModule,
|
||||
HistoryModule,
|
||||
ScoresModule,
|
||||
ScoresModule,
|
||||
MailerModule.forRoot({
|
||||
transport: process.env.SMTP_TRANSPORT,
|
||||
defaults: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
return "Hello World!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,30 +14,31 @@ import {
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateArtistDto } from './dto/create-artist.dto';
|
||||
import { Request } from 'express';
|
||||
import { ArtistService } from './artist.service';
|
||||
import { Prisma, Artist } from '@prisma/client';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateArtistDto } from "./dto/create-artist.dto";
|
||||
import { Request } from "express";
|
||||
import { ArtistService } from "./artist.service";
|
||||
import { Prisma, Artist } from "@prisma/client";
|
||||
import {
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { Public } from 'src/auth/public';
|
||||
} from "@nestjs/swagger";
|
||||
import { createReadStream, existsSync } from "fs";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Public } from "src/auth/public";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
@Controller('artist')
|
||||
@ApiTags('artist')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller("artist")
|
||||
@ApiTags("artist")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class ArtistController {
|
||||
static filterableFields = ['+id', 'name'];
|
||||
static filterableFields = ["+id", "name"];
|
||||
static includableFields: IncludeMap<Prisma.ArtistInclude> = {
|
||||
Song: true,
|
||||
Album: true,
|
||||
@@ -47,7 +48,7 @@ export class ArtistController {
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
description: 'Register a new artist, should not be used by frontend',
|
||||
description: "Register a new artist, should not be used by frontend",
|
||||
})
|
||||
async create(@Body() dto: CreateArtistDto) {
|
||||
try {
|
||||
@@ -57,26 +58,26 @@ export class ArtistController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: 'Delete an artist by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "Delete an artist by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.service.delete({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Get(":id/illustration")
|
||||
@ApiOperation({ description: "Get an artist's illustration" })
|
||||
@ApiNotFoundResponse({ description: 'Artist or illustration not found' })
|
||||
@ApiNotFoundResponse({ description: "Artist or illustration not found" })
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
const artist = await this.service.get({ id });
|
||||
if (!artist) throw new NotFoundException('Artist not found');
|
||||
if (!artist) throw new NotFoundException("Artist not found");
|
||||
const path = `/assets/artists/${artist.name}/illustration.png`;
|
||||
if (!existsSync(path))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(path);
|
||||
@@ -87,15 +88,15 @@ export class ArtistController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ description: 'Get all artists paginated' })
|
||||
@ApiOperation({ description: "Get all artists paginated" })
|
||||
@ApiOkResponsePlaginated(_Artist)
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(ArtistController.filterableFields)
|
||||
where: Prisma.ArtistWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Artist>> {
|
||||
const ret = await this.service.list({
|
||||
skip,
|
||||
@@ -106,20 +107,20 @@ export class ArtistController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: 'Get an artist by id' })
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get an artist by id" })
|
||||
@ApiOkResponse({ type: _Artist })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.service.get(
|
||||
{ id },
|
||||
mapInclude(include, req, ArtistController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Artist not found');
|
||||
if (res === null) throw new NotFoundException("Artist not found");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { ArtistController } from './artist.controller';
|
||||
import { ArtistService } from './artist.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { ArtistController } from "./artist.controller";
|
||||
import { ArtistService } from "./artist.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Artist } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Artist } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class ArtistService {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateArtistDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
5
back/src/auth/apikey-auth.guard.ts
Normal file
5
back/src/auth/apikey-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyAuthGuard extends AuthGuard('api-key') {}
|
||||
28
back/src/auth/apikey.strategy.ts
Normal file
28
back/src/auth/apikey.strategy.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import Strategy from 'passport-headerapikey';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class HeaderApiKeyStrategy extends PassportStrategy(Strategy, 'api-key') {
|
||||
constructor(
|
||||
private readonly configService: ConfigService
|
||||
) {
|
||||
super({ header: 'Authorization', prefix: 'API Key ' },
|
||||
true,
|
||||
async (apiKey, done) => {
|
||||
return this.validate(apiKey, done);
|
||||
});
|
||||
}
|
||||
|
||||
public validate = (apiKey: string, done: (error: Error, data) => {}) => {
|
||||
if (this.configService.get<string>('API_KEYS')?.split(',').includes(apiKey)) {
|
||||
//@ts-expect-error
|
||||
done(null, true);
|
||||
}
|
||||
done(new UnauthorizedException(), null);
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ import {
|
||||
Response,
|
||||
Query,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
import { LocalAuthGuard } from './local-auth.guard';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { UsersService } from 'src/users/users.service';
|
||||
} from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtAuthGuard } from "./jwt-auth.guard";
|
||||
import { LocalAuthGuard } from "./local-auth.guard";
|
||||
import { RegisterDto } from "./dto/register.dto";
|
||||
import { UsersService } from "src/users/users.service";
|
||||
import {
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
@@ -36,21 +36,24 @@ import {
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { User } from '../models/user';
|
||||
import { JwtToken } from './models/jwt';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { Profile } from './dto/profile.dto';
|
||||
import { Setting } from 'src/models/setting';
|
||||
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
|
||||
import { SettingsService } from 'src/settings/settings.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { writeFile } from 'fs';
|
||||
import { PasswordResetDto } from './dto/password_reset.dto ';
|
||||
} from "@nestjs/swagger";
|
||||
import { User } from "../models/user";
|
||||
import { JwtToken } from "./models/jwt";
|
||||
import { LoginDto } from "./dto/login.dto";
|
||||
import { Profile } from "./dto/profile.dto";
|
||||
import { Setting } from "src/models/setting";
|
||||
import { UpdateSettingDto } from "src/settings/dto/update-setting.dto";
|
||||
import { SettingsService } from "src/settings/settings.service";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { writeFile } from "fs";
|
||||
import { PasswordResetDto } from "./dto/password_reset.dto ";
|
||||
import { mapInclude } from "src/utils/include";
|
||||
import { SongController } from "src/song/song.controller";
|
||||
import { ChromaAuthGuard } from "./chroma-auth.guard";
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
@ApiTags("auth")
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
@@ -58,17 +61,17 @@ export class AuthController {
|
||||
private settingsService: SettingsService,
|
||||
) {}
|
||||
|
||||
@Get('login/google')
|
||||
@UseGuards(AuthGuard('google'))
|
||||
@ApiOperation({ description: 'Redirect to google login page' })
|
||||
@Get("login/google")
|
||||
@UseGuards(AuthGuard("google"))
|
||||
@ApiOperation({ description: "Redirect to google login page" })
|
||||
googleLogin() {}
|
||||
|
||||
@Get('logged/google')
|
||||
@Get("logged/google")
|
||||
@ApiOperation({
|
||||
description:
|
||||
'Redirect to the front page after connecting to the google account',
|
||||
"Redirect to the front page after connecting to the google account",
|
||||
})
|
||||
@UseGuards(AuthGuard('google'))
|
||||
@UseGuards(AuthGuard("google"))
|
||||
async googleLoginCallbakc(@Req() req: any) {
|
||||
let user = await this.usersService.user({ googleID: req.user.googleID });
|
||||
if (!user) {
|
||||
@@ -78,13 +81,13 @@ export class AuthController {
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
@ApiOperation({ description: 'Register a new user' })
|
||||
@ApiConflictResponse({ description: 'Username or email already taken' })
|
||||
@Post("register")
|
||||
@ApiOperation({ description: "Register a new user" })
|
||||
@ApiConflictResponse({ description: "Username or email already taken" })
|
||||
@ApiOkResponse({
|
||||
description: 'Successfully registered, email sent to verify',
|
||||
description: "Successfully registered, email sent to verify",
|
||||
})
|
||||
@ApiBadRequestResponse({ description: 'Invalid data or database error' })
|
||||
@ApiBadRequestResponse({ description: "Invalid data or database error" })
|
||||
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
||||
try {
|
||||
const user = await this.usersService.createUser(registerDto);
|
||||
@@ -92,102 +95,102 @@ export class AuthController {
|
||||
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');
|
||||
if (e.code === "P2002") {
|
||||
throw new ConflictException("Username or email already taken");
|
||||
}
|
||||
console.error(e);
|
||||
throw new BadRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
@Put('verify')
|
||||
@Put("verify")
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ description: 'Verify the email of the user' })
|
||||
@ApiOkResponse({ description: 'Successfully verified' })
|
||||
@ApiBadRequestResponse({ description: 'Invalid or expired token' })
|
||||
@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,
|
||||
@Query("token") token: string,
|
||||
): Promise<void> {
|
||||
if (await this.authService.verifyMail(req.user.id, token)) return;
|
||||
throw new BadRequestException('Invalid token. Expired or invalid.');
|
||||
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||
}
|
||||
|
||||
@Put('reverify')
|
||||
@Put("reverify")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Resend the verification email' })
|
||||
@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');
|
||||
if (!user) throw new BadRequestException("Invalid user");
|
||||
await this.authService.sendVerifyMail(user);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Put('password-reset')
|
||||
@Put("password-reset")
|
||||
async password_reset(
|
||||
@Body() resetDto: PasswordResetDto,
|
||||
@Query('token') token: string,
|
||||
@Query("token") token: string,
|
||||
): Promise<void> {
|
||||
if (await this.authService.changePassword(resetDto.password, token)) return;
|
||||
throw new BadRequestException('Invalid token. Expired or invalid.');
|
||||
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Put('forgot-password')
|
||||
async forgot_password(@Query('email') email: string): Promise<void> {
|
||||
@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');
|
||||
if (!user) throw new BadRequestException("Invalid user");
|
||||
await this.authService.sendPasswordResetMail(user);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Post("login")
|
||||
@ApiBody({ type: LoginDto })
|
||||
@HttpCode(200)
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@ApiBody({ type: LoginDto })
|
||||
@ApiOperation({ description: 'Login with username and password' })
|
||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid credentials' })
|
||||
@ApiOperation({ description: "Login with username and password" })
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid credentials" })
|
||||
async login(@Request() req: any): Promise<JwtToken> {
|
||||
return this.authService.login(req.user);
|
||||
}
|
||||
|
||||
@Post('guest')
|
||||
@Post("guest")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Login as a guest account' })
|
||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
||||
@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)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ description: 'Get the profile picture of connected user' })
|
||||
@ApiOkResponse({ description: 'The user profile picture' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me/picture')
|
||||
@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)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'The user profile picture' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Post('me/picture')
|
||||
@ApiOperation({ description: 'Upload a new profile picture' })
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@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',
|
||||
fileType: "jpeg",
|
||||
})
|
||||
.build({
|
||||
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
@@ -203,22 +206,22 @@ export class AuthController {
|
||||
|
||||
@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' })
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me")
|
||||
@ApiOperation({ description: "Get the user info of connected user" })
|
||||
async getProfile(@Request() req: any): Promise<User> {
|
||||
const user = await this.usersService.user({ id: req.user.id });
|
||||
if (!user) throw new InternalServerErrorException();
|
||||
return user;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully edited profile', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Put('me')
|
||||
@ApiOperation({ description: 'Edit the profile of connected user' })
|
||||
@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>,
|
||||
@@ -241,20 +244,20 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully deleted', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Delete('me')
|
||||
@ApiOperation({ description: 'Delete the profile of connected user' })
|
||||
@ApiOkResponse({ description: "Successfully deleted", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Delete("me")
|
||||
@ApiOperation({ description: "Delete the profile of connected user" })
|
||||
deleteSelf(@Request() req: any): Promise<User> {
|
||||
return this.usersService.deleteUser({ id: req.user.id });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Patch('me/settings')
|
||||
@ApiOperation({ description: 'Edit the settings of connected user' })
|
||||
@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,
|
||||
@@ -267,10 +270,10 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me/settings')
|
||||
@ApiOperation({ description: 'Get the settings of connected user' })
|
||||
@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,
|
||||
@@ -281,29 +284,47 @@ export class AuthController {
|
||||
|
||||
@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) {
|
||||
@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) {
|
||||
@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' })
|
||||
@ApiOkResponse({ description: "Successfully retrieved liked song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me/likes")
|
||||
getLikedSongs(@Request() req: any, @Query("include") include: string) {
|
||||
return this.usersService.getLikedSongs(
|
||||
+req.user.id,
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully added score'})
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me/likes')
|
||||
getLikedSongs(@Request() req: any) {
|
||||
return this.usersService.getLikedSongs(+req.user.id);
|
||||
@Patch('me/score/:score')
|
||||
addScore(
|
||||
@Request() req: any,
|
||||
@Param('id') score: number,
|
||||
) {
|
||||
return this.usersService.addScore(
|
||||
+req.user.id,
|
||||
score,
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersModule } from 'src/users/users.module';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { LocalStrategy } from './local.strategy';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { SettingsModule } from 'src/settings/settings.module';
|
||||
import { GoogleStrategy } from './google.strategy';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UsersModule } from "src/users/users.module";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { LocalStrategy } from "./local.strategy";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtStrategy } from "./jwt.strategy";
|
||||
import { SettingsModule } from "src/settings/settings.module";
|
||||
import { GoogleStrategy } from "./google.strategy";
|
||||
import { HeaderApiKeyStrategy } from "./apikey.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -20,13 +21,19 @@ import { GoogleStrategy } from './google.strategy';
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '365d' },
|
||||
secret: configService.get("JWT_SECRET"),
|
||||
signOptions: { expiresIn: "365d" },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy],
|
||||
providers: [
|
||||
AuthService,
|
||||
LocalStrategy,
|
||||
JwtStrategy,
|
||||
GoogleStrategy,
|
||||
HeaderApiKeyStrategy,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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';
|
||||
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(
|
||||
@@ -13,6 +13,13 @@ export class AuthService {
|
||||
private emailService: MailerService,
|
||||
) {}
|
||||
|
||||
validateApiKey(apikey: string): boolean {
|
||||
if (process.env.API_KEYS == null) return false;
|
||||
const keys = process.env.API_KEYS.split(',');
|
||||
return keys.includes(apikey);
|
||||
|
||||
}
|
||||
|
||||
async validateUser(
|
||||
username: string,
|
||||
password: string,
|
||||
@@ -36,37 +43,37 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async sendVerifyMail(user: User) {
|
||||
if (process.env.IGNORE_MAILS === 'true') return;
|
||||
if (process.env.IGNORE_MAILS === "true") return;
|
||||
if (user.email == null) return;
|
||||
console.log('Sending verification mail to', user.email);
|
||||
console.log("Sending verification mail to", user.email);
|
||||
const token = await this.jwtService.signAsync(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{ expiresIn: '10h' },
|
||||
{ expiresIn: "10h" },
|
||||
);
|
||||
await this.emailService.sendMail({
|
||||
to: user.email,
|
||||
from: 'chromacase@octohub.app',
|
||||
subject: 'Mail verification for Chromacase',
|
||||
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 (process.env.IGNORE_MAILS === "true") return;
|
||||
if (user.email == null) return;
|
||||
console.log('Sending password reset mail to', user.email);
|
||||
console.log("Sending password reset mail to", user.email);
|
||||
const token = await this.jwtService.signAsync(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{ expiresIn: '10h' },
|
||||
{ expiresIn: "10h" },
|
||||
);
|
||||
await this.emailService.sendMail({
|
||||
to: user.email,
|
||||
from: 'chromacase@octohub.app',
|
||||
subject: 'Password reset for Chromacase',
|
||||
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>.`,
|
||||
});
|
||||
}
|
||||
@@ -76,7 +83,7 @@ export class AuthService {
|
||||
try {
|
||||
verified = await this.jwtService.verifyAsync(token);
|
||||
} catch (e) {
|
||||
console.log('Password reset token failure', e);
|
||||
console.log("Password reset token failure", e);
|
||||
return false;
|
||||
}
|
||||
console.log(verified);
|
||||
@@ -91,7 +98,7 @@ export class AuthService {
|
||||
try {
|
||||
await this.jwtService.verifyAsync(token);
|
||||
} catch (e) {
|
||||
console.log('Verify mail token failure', e);
|
||||
console.log("Verify mail token failure", e);
|
||||
return false;
|
||||
}
|
||||
await this.userService.updateUser({
|
||||
|
||||
22
back/src/auth/chroma-auth.guard.ts
Normal file
22
back/src/auth/chroma-auth.guard.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { IS_PUBLIC_KEY } from "./public";
|
||||
|
||||
@Injectable()
|
||||
export class ChromaAuthGuard extends AuthGuard(["jwt", "api-key"]) {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class Constants {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getSecret = () => {
|
||||
return this.configService.get('JWT_SECRET');
|
||||
return this.configService.get("JWT_SECRET");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class PasswordResetDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class Profile {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from '@prisma/client';
|
||||
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) {
|
||||
@@ -10,7 +10,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
scope: ['email', 'profile'],
|
||||
scope: ["email", "profile"],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { IS_PUBLIC_KEY } from './public';
|
||||
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { IS_PUBLIC_KEY } from "./public";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
console.log(context);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
console.log(isPublic);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -9,7 +9,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
secretOrKey: configService.get("JWT_SECRET"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||
export class LocalAuthGuard extends AuthGuard("local") {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Strategy } from 'passport-local';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import PayloadInterface from './interface/payload.interface';
|
||||
import { Strategy } from "passport-local";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import PayloadInterface from "./interface/payload.interface";
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class JwtToken {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const IS_PUBLIC_KEY = "isPublic";
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateGenreDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -14,25 +14,26 @@ import {
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateGenreDto } from './dto/create-genre.dto';
|
||||
import { Request } from 'express';
|
||||
import { GenreService } from './genre.service';
|
||||
import { Prisma, Genre } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { Public } from 'src/auth/public';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateGenreDto } from "./dto/create-genre.dto";
|
||||
import { Request } from "express";
|
||||
import { GenreService } from "./genre.service";
|
||||
import { Prisma, Genre } from "@prisma/client";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { createReadStream, existsSync } from "fs";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Genre as _Genre } from "src/_gen/prisma-class/genre";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Public } from "src/auth/public";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
@Controller('genre')
|
||||
@ApiTags('genre')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller("genre")
|
||||
@ApiTags("genre")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class GenreController {
|
||||
static filterableFields: string[] = ['+id', 'name'];
|
||||
static filterableFields: string[] = ["+id", "name"];
|
||||
static includableFields: IncludeMap<Prisma.GenreInclude> = {
|
||||
Song: true,
|
||||
};
|
||||
@@ -48,23 +49,23 @@ export class GenreController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.service.delete({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Get(":id/illustration")
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
const genre = await this.service.get({ id });
|
||||
if (!genre) throw new NotFoundException('Genre not found');
|
||||
if (!genre) throw new NotFoundException("Genre not found");
|
||||
const path = `/assets/genres/${genre.name}/illustration.png`;
|
||||
if (!existsSync(path))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(path);
|
||||
@@ -80,9 +81,9 @@ export class GenreController {
|
||||
@Req() req: Request,
|
||||
@FilterQuery(GenreController.filterableFields)
|
||||
where: Prisma.GenreWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Genre>> {
|
||||
const ret = await this.service.list({
|
||||
skip,
|
||||
@@ -93,18 +94,18 @@ export class GenreController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(":id")
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.service.get(
|
||||
{ id },
|
||||
mapInclude(include, req, GenreController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Genre not found');
|
||||
if (res === null) throw new NotFoundException("Genre not found");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { GenreController } from './genre.controller';
|
||||
import { GenreService } from './genre.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { GenreController } from "./genre.controller";
|
||||
import { GenreService } from "./genre.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Genre } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Genre } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class GenreService {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -9,68 +9,75 @@ import {
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
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';
|
||||
} from "@nestjs/swagger";
|
||||
import { SearchHistory, SongHistory } from "@prisma/client";
|
||||
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||
import { HistoryService } from "./history.service";
|
||||
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||
import { SongHistory as _SongHistory } from "src/_gen/prisma-class/song_history";
|
||||
import { SearchHistory as _SearchHistory } from "src/_gen/prisma-class/search_history";
|
||||
import { SongController } from "src/song/song.controller";
|
||||
import { mapInclude } from "src/utils/include";
|
||||
|
||||
@Controller('history')
|
||||
@ApiTags('history')
|
||||
@Controller("history")
|
||||
@ApiTags("history")
|
||||
export class HistoryController {
|
||||
constructor(private readonly historyService: HistoryService) {}
|
||||
|
||||
@Get()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Get song history of connected user' })
|
||||
@ApiOperation({ description: "Get song history of connected user" })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _SongHistory, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async getHistory(
|
||||
@Request() req: any,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
): Promise<SongHistory[]> {
|
||||
return this.historyService.getHistory(req.user.id, { skip, take });
|
||||
return this.historyService.getHistory(
|
||||
req.user.id,
|
||||
{ skip, take },
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
);
|
||||
}
|
||||
|
||||
@Get('search')
|
||||
@Get("search")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Get search history of connected user' })
|
||||
@ApiOperation({ description: "Get search history of connected user" })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _SearchHistory, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async getSearchHistory(
|
||||
@Request() req: any,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<SearchHistory[]> {
|
||||
return this.historyService.getSearchHistory(req.user.id, { skip, take });
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(201)
|
||||
@ApiOperation({ description: 'Create a record of a song played by a user' })
|
||||
@ApiCreatedResponse({ description: 'Succesfully created a record' })
|
||||
@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' })
|
||||
@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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { HistoryService } from './history.service';
|
||||
import { HistoryController } from './history.controller';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { HistoryService } from "./history.service";
|
||||
import { HistoryController } from "./history.controller";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HistoryService } from './history.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { HistoryService } from "./history.service";
|
||||
|
||||
describe('HistoryService', () => {
|
||||
describe("HistoryService", () => {
|
||||
let service: HistoryService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('HistoryService', () => {
|
||||
service = module.get<HistoryService>(HistoryService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, SearchHistory, SongHistory } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||
|
||||
@Injectable()
|
||||
export class HistoryService {
|
||||
@@ -45,12 +45,14 @@ export class HistoryService {
|
||||
async getHistory(
|
||||
playerId: number,
|
||||
{ skip, take }: { skip?: number; take?: number },
|
||||
include?: Prisma.SongInclude,
|
||||
): Promise<SongHistory[]> {
|
||||
return this.prisma.songHistory.findMany({
|
||||
where: { user: { id: playerId } },
|
||||
orderBy: { playDate: 'desc' },
|
||||
orderBy: { playDate: "desc" },
|
||||
skip,
|
||||
take,
|
||||
include: { song: include ? { include } : true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +65,7 @@ export class HistoryService {
|
||||
}): Promise<{ best: number; history: SongHistory[] }> {
|
||||
const history = await this.prisma.songHistory.findMany({
|
||||
where: { user: { id: playerId }, song: { id: songId } },
|
||||
orderBy: { playDate: 'desc' },
|
||||
orderBy: { playDate: "desc" },
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -95,7 +97,7 @@ export class HistoryService {
|
||||
): Promise<SearchHistory[]> {
|
||||
return this.prisma.searchHistory.findMany({
|
||||
where: { user: { id: playerId } },
|
||||
orderBy: { searchDate: 'desc' },
|
||||
orderBy: { searchDate: "desc" },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
|
||||
@@ -12,16 +12,17 @@ import {
|
||||
Delete,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { LessonService } from './lesson.service';
|
||||
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { Prisma, Skill } from '@prisma/client';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { Request } from 'express';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { LessonService } from "./lesson.service";
|
||||
import { ApiOperation, ApiProperty, ApiTags } from "@nestjs/swagger";
|
||||
import { Prisma, Skill } from "@prisma/client";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Lesson as _Lesson } from "src/_gen/prisma-class/lesson";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Request } from "express";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
export class Lesson {
|
||||
@ApiProperty()
|
||||
@@ -36,15 +37,15 @@ export class Lesson {
|
||||
mainSkill: Skill;
|
||||
}
|
||||
|
||||
@ApiTags('lessons')
|
||||
@Controller('lesson')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiTags("lessons")
|
||||
@Controller("lesson")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class LessonController {
|
||||
static filterableFields: string[] = [
|
||||
'+id',
|
||||
'name',
|
||||
'+requiredLevel',
|
||||
'mainSkill',
|
||||
"+id",
|
||||
"name",
|
||||
"+requiredLevel",
|
||||
"mainSkill",
|
||||
];
|
||||
static includableFields: IncludeMap<Prisma.LessonInclude> = {
|
||||
LessonHistory: true,
|
||||
@@ -53,7 +54,7 @@ export class LessonController {
|
||||
constructor(private lessonService: LessonService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Get all lessons',
|
||||
summary: "Get all lessons",
|
||||
})
|
||||
@Get()
|
||||
@ApiOkResponsePlaginated(_Lesson)
|
||||
@@ -61,9 +62,9 @@ export class LessonController {
|
||||
@Req() request: Request,
|
||||
@FilterQuery(LessonController.filterableFields)
|
||||
where: Prisma.LessonWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Lesson>> {
|
||||
const ret = await this.lessonService.getAll({
|
||||
skip,
|
||||
@@ -75,13 +76,13 @@ export class LessonController {
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Get a particular lessons',
|
||||
summary: "Get a particular lessons",
|
||||
})
|
||||
@Get(':id')
|
||||
@Get(":id")
|
||||
async get(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
): Promise<Lesson> {
|
||||
const ret = await this.lessonService.get(
|
||||
id,
|
||||
@@ -92,7 +93,7 @@ export class LessonController {
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Create a lessons',
|
||||
summary: "Create a lessons",
|
||||
})
|
||||
@Post()
|
||||
async post(@Body() lesson: Lesson): Promise<Lesson> {
|
||||
@@ -105,10 +106,10 @@ export class LessonController {
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Delete a lessons',
|
||||
summary: "Delete a lessons",
|
||||
})
|
||||
@Delete(':id')
|
||||
async delete(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
||||
@Delete(":id")
|
||||
async delete(@Param("id", ParseIntPipe) id: number): Promise<Lesson> {
|
||||
try {
|
||||
return await this.lessonService.delete(id);
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { LessonController } from './lesson.controller';
|
||||
import { LessonService } from './lesson.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { LessonController } from "./lesson.controller";
|
||||
import { LessonService } from "./lesson.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LessonService } from './lesson.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { LessonService } from "./lesson.service";
|
||||
|
||||
describe('LessonService', () => {
|
||||
describe("LessonService", () => {
|
||||
let service: LessonService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('LessonService', () => {
|
||||
service = module.get<LessonService>(LessonService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Lesson, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Lesson, Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class LessonService {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
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';
|
||||
} 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 {
|
||||
@@ -27,8 +27,8 @@ export class AspectLogger implements NestInterceptor {
|
||||
params,
|
||||
query,
|
||||
body,
|
||||
userId: user?.id ?? 'not logged in',
|
||||
username: user?.username ?? 'not logged in',
|
||||
userId: user?.id ?? "not logged in",
|
||||
username: user?.username ?? "not logged in",
|
||||
};
|
||||
|
||||
return next.handle().pipe(
|
||||
@@ -48,20 +48,20 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.use(
|
||||
RequestLogger.buildExpressRequestLogger({
|
||||
doNotLogPaths: ['/health'],
|
||||
doNotLogPaths: ["/health"],
|
||||
} as RequestLoggerOptions),
|
||||
);
|
||||
app.enableShutdownHooks();
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Chromacase')
|
||||
.setDescription('The chromacase API')
|
||||
.setVersion('1.0')
|
||||
.setTitle("Chromacase")
|
||||
.setDescription("The chromacase API")
|
||||
.setVersion("1.0")
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, config, {
|
||||
extraModels: [...PrismaModel.extraModels],
|
||||
});
|
||||
SwaggerModule.setup('api', app, document);
|
||||
SwaggerModule.setup("api", app, document);
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
app.enableCors();
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
|
||||
*/
|
||||
|
||||
import { Type, applyDecorators } from '@nestjs/common';
|
||||
import { Type, applyDecorators } from "@nestjs/common";
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOkResponse,
|
||||
ApiProperty,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
} from "@nestjs/swagger";
|
||||
|
||||
export class PlageMetadata {
|
||||
@ApiProperty()
|
||||
this: string;
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description: "null if there is no next page, couldn't set it in swagger",
|
||||
})
|
||||
next: string | null;
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description:
|
||||
"null if there is no previous page, couldn't set it in swagger",
|
||||
@@ -35,9 +35,9 @@ export class Plage<T extends object> {
|
||||
|
||||
constructor(data: T[], request: Request | any) {
|
||||
this.data = data;
|
||||
let take = Number(request.query['take'] ?? 20).valueOf();
|
||||
let take = Number(request.query["take"] ?? 20).valueOf();
|
||||
if (take == 0) take = 20;
|
||||
let skipped: number = Number(request.query['skip'] ?? 0).valueOf();
|
||||
let skipped: number = Number(request.query["skip"] ?? 0).valueOf();
|
||||
if (skipped % take) {
|
||||
skipped += take - (skipped % take);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
|
||||
{
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
type: "array",
|
||||
items: { $ref: getSchemaPath(dataDto) },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class Setting {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class User {
|
||||
@ApiProperty()
|
||||
@@ -11,4 +11,6 @@ export class User {
|
||||
isGuest: boolean;
|
||||
@ApiProperty()
|
||||
partyPlayed: number;
|
||||
@ApiProperty()
|
||||
totalScore: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaService } from "./prisma.service";
|
||||
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PrismaService } from "./prisma.service";
|
||||
|
||||
describe('PrismaService', () => {
|
||||
describe("PrismaService", () => {
|
||||
let service: PrismaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('PrismaService', () => {
|
||||
service = module.get<PrismaService>(PrismaService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { ScoresService } from './scores.service';
|
||||
import { User } from '@prisma/client';
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ScoresService } from "./scores.service";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
@ApiTags('scores')
|
||||
@Controller('scores')
|
||||
@ApiTags("scores")
|
||||
@Controller("scores")
|
||||
export class ScoresController {
|
||||
constructor(private readonly scoresService: ScoresService) {}
|
||||
|
||||
|
||||
@ApiOkResponse({ description: 'Successfully sent the Top 20 players'})
|
||||
@Get('top/20')
|
||||
@ApiOkResponse({ description: "Successfully sent the Top 20 players" })
|
||||
@Get("top/20")
|
||||
getTopTwenty(): Promise<User[]> {
|
||||
return this.scoresService.topTwenty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class SearchSongDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -7,38 +7,38 @@ import {
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { Artist, Genre, Song } from '@prisma/client';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { SearchService } from './search.service';
|
||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
||||
import { mapInclude } from 'src/utils/include';
|
||||
import { SongController } from 'src/song/song.controller';
|
||||
import { GenreController } from 'src/genre/genre.controller';
|
||||
import { ArtistController } from 'src/artist/artist.controller';
|
||||
} from "@nestjs/swagger";
|
||||
import { Artist, Genre, Song } from "@prisma/client";
|
||||
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||
import { SearchService } from "./search.service";
|
||||
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||
import { Genre as _Genre } from "src/_gen/prisma-class/genre";
|
||||
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||
import { mapInclude } from "src/utils/include";
|
||||
import { SongController } from "src/song/song.controller";
|
||||
import { GenreController } from "src/genre/genre.controller";
|
||||
import { ArtistController } from "src/artist/artist.controller";
|
||||
|
||||
@ApiTags('search')
|
||||
@Controller('search')
|
||||
@ApiTags("search")
|
||||
@Controller("search")
|
||||
export class SearchController {
|
||||
constructor(private readonly searchService: SearchService) {}
|
||||
|
||||
@Get('songs/:query')
|
||||
@Get("songs/:query")
|
||||
@ApiOkResponse({ type: _Song, isArray: true })
|
||||
@ApiOperation({ description: 'Search a song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiOperation({ description: "Search a song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async searchSong(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
@Query("include") include: string,
|
||||
@Param("query") query: string,
|
||||
): Promise<Song[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.songByGuess(
|
||||
@@ -53,15 +53,15 @@ export class SearchController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get('genres/:query')
|
||||
@Get("genres/:query")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@ApiOkResponse({ type: _Genre, isArray: true })
|
||||
@ApiOperation({ description: 'Search a genre' })
|
||||
@ApiOperation({ description: "Search a genre" })
|
||||
async searchGenre(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
@Query("include") include: string,
|
||||
@Param("query") query: string,
|
||||
): Promise<Genre[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.genreByGuess(
|
||||
@@ -76,15 +76,15 @@ export class SearchController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get('artists/:query')
|
||||
@Get("artists/:query")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiOperation({ description: 'Search an artist' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@ApiOperation({ description: "Search an artist" })
|
||||
async searchArtists(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
@Query("include") include: string,
|
||||
@Param("query") query: string,
|
||||
): Promise<Artist[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.artistByGuess(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SearchService } from './search.service';
|
||||
import { SearchController } from './search.controller';
|
||||
import { HistoryModule } from 'src/history/history.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { SongService } from 'src/song/song.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SearchService } from "./search.service";
|
||||
import { SearchController } from "./search.controller";
|
||||
import { HistoryModule } from "src/history/history.module";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { SongService } from "src/song/song.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, HistoryModule],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Artist, Prisma, Song, Genre } from '@prisma/client';
|
||||
import { HistoryService } from 'src/history/history.service';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Artist, Prisma, Song, Genre } from "@prisma/client";
|
||||
import { HistoryService } from "src/history/history.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class SearchService {
|
||||
@@ -17,7 +17,7 @@ export class SearchService {
|
||||
): Promise<Song[]> {
|
||||
return this.prisma.song.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
include,
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export class SearchService {
|
||||
): Promise<Genre[]> {
|
||||
return this.prisma.genre.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
include,
|
||||
});
|
||||
@@ -43,7 +43,7 @@ export class SearchService {
|
||||
): Promise<Artist[]> {
|
||||
return this.prisma.artist.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
include,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class UpdateSettingDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SettingsService } from "./settings.service";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, UserSettings } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, UserSettings } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class SettingsService {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateSongDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -38,7 +38,7 @@ import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||
import { SongHistory } from "src/_gen/prisma-class/song_history";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Public } from "src/auth/public";
|
||||
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
class SongHistoryResult {
|
||||
@ApiProperty()
|
||||
best: number;
|
||||
@@ -48,7 +48,7 @@ class SongHistoryResult {
|
||||
|
||||
@Controller("song")
|
||||
@ApiTags("song")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class SongController {
|
||||
static filterableFields: string[] = [
|
||||
"+id",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SongService } from './song.service';
|
||||
import { SongController } from './song.controller';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { HistoryModule } from 'src/history/history.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SongService } from "./song.service";
|
||||
import { SongController } from "./song.controller";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { HistoryModule } from "src/history/history.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, HistoryModule],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SongService } from './song.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { SongService } from "./song.service";
|
||||
|
||||
describe('SongService', () => {
|
||||
describe("SongService", () => {
|
||||
let service: SongService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('SongService', () => {
|
||||
service = module.get<SongService>(SongService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
import { PartialType } from "@nestjs/mapped-types";
|
||||
import { CreateUserDto } from "./create-user.dto";
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
Param,
|
||||
NotFoundException,
|
||||
Response,
|
||||
} from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { User } from 'src/models/user';
|
||||
} from "@nestjs/common";
|
||||
import { UsersService } from "./users.service";
|
||||
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { User } from "src/models/user";
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
@ApiTags("users")
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@@ -20,19 +20,19 @@ export class UsersController {
|
||||
return this.usersService.users({});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(":id")
|
||||
@ApiNotFoundResponse()
|
||||
async findOne(@Param('id') id: number): Promise<User> {
|
||||
async findOne(@Param("id") id: number): Promise<User> {
|
||||
const ret = await this.usersService.user({ id: +id });
|
||||
if (!ret) throw new NotFoundException();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Get(':id/picture')
|
||||
@Get(":id/picture")
|
||||
@ApiOkResponse({
|
||||
description: 'Return the profile picture of the requested user',
|
||||
description: "Return the profile picture of the requested user",
|
||||
})
|
||||
async getPicture(@Response() res: any, @Param('id') id: number) {
|
||||
async getPicture(@Response() res: any, @Param("id") id: number) {
|
||||
return await this.usersService.getProfilePicture(+id, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersController } from './users.controller';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { SettingsService } from 'src/settings/settings.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UsersService } from "./users.service";
|
||||
import { UsersController } from "./users.controller";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { SettingsService } from "src/settings/settings.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -2,13 +2,13 @@ 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 { createHash, randomUUID } from 'crypto';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
} from "@nestjs/common";
|
||||
import { User, Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import * as bcrypt from "bcryptjs";
|
||||
import { createHash, randomUUID } from "crypto";
|
||||
import { createReadStream, existsSync } from "fs";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
@@ -53,7 +53,7 @@ export class UsersService {
|
||||
isGuest: true,
|
||||
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
|
||||
email: null,
|
||||
password: '',
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class UsersService {
|
||||
data: Prisma.UserUpdateInput;
|
||||
}): Promise<User> {
|
||||
const { where, data } = params;
|
||||
if (typeof data.password === 'string')
|
||||
if (typeof data.password === "string")
|
||||
data.password = await bcrypt.hash(data.password, 8);
|
||||
else if (data.password && data.password.set)
|
||||
data.password = await bcrypt.hash(data.password.set, 8);
|
||||
@@ -89,9 +89,9 @@ export class UsersService {
|
||||
const user = await this.user({ id: userId });
|
||||
if (!user) throw new InternalServerErrorException();
|
||||
if (!user.email) throw new NotFoundException();
|
||||
const hash = createHash('md5')
|
||||
const hash = createHash("md5")
|
||||
.update(user.email.trim().toLowerCase())
|
||||
.digest('hex');
|
||||
.digest("hex");
|
||||
const resp = await fetch(
|
||||
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
|
||||
);
|
||||
@@ -105,9 +105,10 @@ export class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
async getLikedSongs(userId: number) {
|
||||
async getLikedSongs(userId: number, include?: Prisma.SongInclude) {
|
||||
return this.prisma.likedSongs.findMany({
|
||||
where: { userId: userId },
|
||||
include: { song: include ? { include } : true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Injectable,
|
||||
PipeTransform,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class FilterPipe implements PipeTransform {
|
||||
@@ -12,13 +12,13 @@ export class FilterPipe implements PipeTransform {
|
||||
transform(value: Record<string, string>) {
|
||||
const filter = {};
|
||||
for (const fieldIdentifier of this.fields) {
|
||||
const field = fieldIdentifier.startsWith('+')
|
||||
const field = fieldIdentifier.startsWith("+")
|
||||
? fieldIdentifier.slice(1)
|
||||
: fieldIdentifier;
|
||||
|
||||
if (value[field] === undefined) continue;
|
||||
|
||||
if (fieldIdentifier.startsWith('+')) {
|
||||
if (fieldIdentifier.startsWith("+")) {
|
||||
filter[field] = parseInt(value[field]);
|
||||
if (isNaN(filter[field]))
|
||||
throw new BadRequestException(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request } from 'express';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Request } from "express";
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
|
||||
export type IncludeMap<IncludeType> = {
|
||||
[key in keyof IncludeType]:
|
||||
@@ -15,9 +15,9 @@ export function mapInclude<IncludeType>(
|
||||
if (!include) return undefined;
|
||||
|
||||
const ret: IncludeType = {} as IncludeType;
|
||||
for (const key of include.split(',')) {
|
||||
for (const key of include.split(",")) {
|
||||
const value =
|
||||
typeof fields[key] === 'function'
|
||||
typeof fields[key] === "function"
|
||||
? fields[key]({ user: req.user })
|
||||
: fields[key];
|
||||
if (value !== false && value !== undefined) ret[key] = value;
|
||||
@@ -25,7 +25,7 @@ export function mapInclude<IncludeType>(
|
||||
throw new BadRequestException(
|
||||
`Invalid include, ${key} is not valid. Valid includes are: ${Object.keys(
|
||||
fields,
|
||||
).join(', ')}.`,
|
||||
).join(", ")}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from './../src/app.module';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import * as request from "supertest";
|
||||
import { AppModule } from "./../src/app.module";
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
describe("AppController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -15,10 +15,10 @@ describe('AppController (e2e)', () => {
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
it("/ (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.get("/")
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
.expect("Hello World!");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ Documentation Tests of the /album route.
|
||||
... Ensures that the album CRUD works corectly.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ../auth/auth.resource
|
||||
Test Setup ApiKey
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
|
||||
@@ -3,6 +3,8 @@ Documentation Tests of the /artist route.
|
||||
... Ensures that the artist CRUD works corectly.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ../auth/auth.resource
|
||||
Test Setup ApiKey
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
|
||||
@@ -5,6 +5,11 @@ Resource ../rest.resource
|
||||
|
||||
|
||||
*** Keywords ***
|
||||
ApiKey
|
||||
[Documentation] Set the API Key
|
||||
Set Headers {"Authorization": "API Key %{API_KEY_ROBOT}"}
|
||||
|
||||
|
||||
Login
|
||||
[Documentation] Shortcut to login with the given username for future requests
|
||||
[Arguments] ${username}
|
||||
|
||||
@@ -4,6 +4,8 @@ Documentation Tests of the /auth route.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ./auth.resource
|
||||
Test Setup ApiKey
|
||||
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
|
||||
@@ -66,7 +66,7 @@ GuestToNormal
|
||||
Integer response status 200
|
||||
Boolean response body isGuest true
|
||||
|
||||
${res}= PUT /auth/me { "username": "toto", "password": "toto", "email": "a@b.c"}
|
||||
${res}= PUT /auth/me { "username": "toto", "password": "toto", "email": "awdaw@b.c"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body username "toto"
|
||||
|
||||
@@ -3,6 +3,8 @@ Documentation Tests of the /genre route.
|
||||
... Ensures that the genre CRUD works corectly.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ../auth/auth.resource
|
||||
Test Setup ApiKey
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
|
||||
@@ -4,6 +4,7 @@ Documentation Tests of the /history route.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ../auth/auth.resource
|
||||
Test Setup ApiKey
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
|
||||
@@ -3,6 +3,8 @@ Documentation Tests of the /lesson route.
|
||||
... Ensures that the lesson CRUD works corectly.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ../auth/auth.resource
|
||||
Test Setup ApiKey
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
|
||||
@@ -4,6 +4,8 @@ Documentation Tests of the /song route.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ../auth/auth.resource
|
||||
Resource ../auth/auth.resource
|
||||
Test Setup ApiKey
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-restricted-imports": [
|
||||
|
||||
2
front/.gitignore
vendored
2
front/.gitignore
vendored
@@ -3,6 +3,8 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
.idea/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
.expo-shared/
|
||||
dist/
|
||||
.vscode/
|
||||
.storybook/
|
||||
.storybook/
|
||||
android/
|
||||
66
front/API.ts
66
front/API.ts
@@ -4,8 +4,8 @@ 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 { LikedSong, LikedSongHandler } from './models/LikedSong';
|
||||
import Song, { SongHandler, SongInclude } from './models/Song';
|
||||
import { SongHistoryHandler, SongHistoryItem, SongHistoryItemHandler } from './models/SongHistory';
|
||||
import User, { UserHandler } from './models/User';
|
||||
import store from './state/Store';
|
||||
@@ -139,6 +139,7 @@ export default class API {
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
if (!handler.transformer) return handler.validator.cast(validated);
|
||||
return handler.transformer(handler.validator.cast(validated));
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) throw new Error("Error while parsing Server's response");
|
||||
@@ -275,13 +276,14 @@ export default class API {
|
||||
};
|
||||
}
|
||||
|
||||
public static getAllSongs(): Query<Song[]> {
|
||||
public static getAllSongs(include?: SongInclude[]): Query<Song[]> {
|
||||
include ??= [];
|
||||
return {
|
||||
key: 'songs',
|
||||
key: ['songs', include],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/song',
|
||||
route: `/song?include=${include!.join(',')}`,
|
||||
},
|
||||
{
|
||||
handler: PlageHandler(SongHandler),
|
||||
@@ -294,13 +296,14 @@ export default class API {
|
||||
* Retrieve a song
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static getSong(songId: number): Query<Song> {
|
||||
public static getSong(songId: number, include?: SongInclude[]): Query<Song> {
|
||||
include ??= [];
|
||||
return {
|
||||
key: ['song', songId],
|
||||
key: ['song', songId, include],
|
||||
exec: async () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}`,
|
||||
route: `/song/${songId}?include=${include!.join(',')}`,
|
||||
},
|
||||
{ handler: SongHandler }
|
||||
),
|
||||
@@ -330,13 +333,15 @@ export default class API {
|
||||
* @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[]> {
|
||||
public static getSongsByGenre(genreId: number, includes?: SongInclude[]): Query<Song[]> {
|
||||
includes ??= [];
|
||||
|
||||
return {
|
||||
key: ['genre', genreId, 'songs'],
|
||||
key: ['genre', genreId, 'songs', includes],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song?genreId=${genreId}`,
|
||||
route: `/song?genreId=${genreId}&includes=${includes!.join(',')}`,
|
||||
},
|
||||
{ handler: PlageHandler(SongHandler) }
|
||||
).then(({ data }) => data),
|
||||
@@ -629,21 +634,22 @@ export default class API {
|
||||
* Retrieve the authenticated user's recommendations
|
||||
* @returns an array of songs
|
||||
*/
|
||||
public static getSongSuggestions(): Query<Song[]> {
|
||||
return API.getAllSongs();
|
||||
public static getSongSuggestions(include?: SongInclude[]): Query<Song[]> {
|
||||
return API.getAllSongs(include);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authenticated user's play history
|
||||
* * @returns an array of songs
|
||||
*/
|
||||
public static getUserPlayHistory(): Query<SongHistoryItem[]> {
|
||||
public static getUserPlayHistory(include?: SongInclude[]): Query<SongHistoryItem[]> {
|
||||
include ??= [];
|
||||
return {
|
||||
key: ['history'],
|
||||
key: ['history', include],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/history',
|
||||
route: `/history?include=${include!.join(',')}`,
|
||||
},
|
||||
{ handler: ListHandler(SongHistoryItemHandler) }
|
||||
),
|
||||
@@ -722,13 +728,14 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
public static getLikedSongs(): Query<likedSong[]> {
|
||||
public static getLikedSongs(include?: SongInclude[]): Query<LikedSong[]> {
|
||||
include ??= [];
|
||||
return {
|
||||
key: ['liked songs'],
|
||||
key: ['liked songs', include],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/auth/me/likes',
|
||||
route: `/auth/me/likes?include=${include!.join(',')}`,
|
||||
},
|
||||
{ handler: ListHandler(LikedSongHandler) }
|
||||
),
|
||||
@@ -772,4 +779,25 @@ export default class API {
|
||||
public static getPartitionSvgUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||
}
|
||||
|
||||
public static async updateUserTotalScore(score: number): Promise<void> {
|
||||
await API.fetch({
|
||||
route: `/auth/me/score/${score}`,
|
||||
method: 'PATCH',
|
||||
});
|
||||
}
|
||||
|
||||
public static getTopTwentyPlayers(): Query<User[]> {
|
||||
return {
|
||||
key: ['score'],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/scores/top/20',
|
||||
method: 'GET',
|
||||
},
|
||||
{ handler: ListHandler(UserHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import ForgotPasswordView from './views/ForgotPasswordView';
|
||||
import Leaderboardiew from './views/LeaderboardView';
|
||||
import DiscoveryView from './views/V2/DiscoveryView';
|
||||
import MusicView from './views/MusicView';
|
||||
import Leaderboardiew from './views/LeaderboardView';
|
||||
|
||||
// Util function to hide route props in URL
|
||||
const removeMe = () => '';
|
||||
|
||||
@@ -70,11 +70,4 @@ const transformQuery = <OldReturnType, NewReturnType>(
|
||||
};
|
||||
};
|
||||
|
||||
const useQueries = <ReturnTypes>(
|
||||
queries: readonly QueryOrQueryFn<ReturnTypes>[],
|
||||
options?: QueryOptions<ReturnTypes>
|
||||
) => {
|
||||
return RQ.useQueries(queries.map((q) => buildRQuery(q, options)));
|
||||
};
|
||||
|
||||
export { useQuery, useQueries, QueryRules, transformQuery };
|
||||
export { useQuery, QueryRules, transformQuery };
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { HStack, IconButton, Image, Text } from 'native-base';
|
||||
import RowCustom from './RowCustom';
|
||||
import TextButton from './TextButton';
|
||||
import { LikedSongWithDetails } from '../models/LikedSong';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import API from '../API';
|
||||
import DurationComponent from './DurationComponent';
|
||||
import Song from '../models/Song';
|
||||
|
||||
type FavSongRowProps = {
|
||||
FavSong: LikedSongWithDetails; // TODO: remove Song
|
||||
song: Song;
|
||||
addedDate: Date;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
||||
const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
|
||||
return (
|
||||
<RowCustom width={'100%'}>
|
||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||
@@ -20,8 +21,8 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
||||
flexGrow={0}
|
||||
pl={10}
|
||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
||||
source={{ uri: FavSong.details.cover }}
|
||||
alt={FavSong.details.name}
|
||||
source={{ uri: song.cover }}
|
||||
alt={song.name}
|
||||
borderColor={'white'}
|
||||
borderWidth={1}
|
||||
/>
|
||||
@@ -45,7 +46,7 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
||||
bold
|
||||
fontSize="md"
|
||||
>
|
||||
{FavSong.details.name}
|
||||
{song.name}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -53,16 +54,16 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{FavSong.addedDate.toLocaleDateString()}
|
||||
{addedDate.toLocaleDateString()}
|
||||
</Text>
|
||||
<DurationComponent length={FavSong.details.details.length} />
|
||||
<DurationComponent length={song.difficulties.length} />
|
||||
</HStack>
|
||||
<IconButton
|
||||
colorScheme="primary"
|
||||
variant={'ghost'}
|
||||
borderRadius={'full'}
|
||||
onPress={() => {
|
||||
API.removeLikedSong(FavSong.songId);
|
||||
API.removeLikedSong(song.id);
|
||||
}}
|
||||
_icon={{
|
||||
as: MaterialIcons,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Heading,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'native-base';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { SearchContext } from '../views/SearchView';
|
||||
import { useQueries, useQuery } from '../Queries';
|
||||
import { useQuery } from '../Queries';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import API from '../API';
|
||||
import LoadingComponent, { LoadingView } from './Loading';
|
||||
@@ -21,17 +21,15 @@ import GenreCard from './GenreCard';
|
||||
import SongCard from './SongCard';
|
||||
import CardGridCustom from './CardGridCustom';
|
||||
import SearchHistoryCard from './HistoryCard';
|
||||
import Song, { SongWithArtist } from '../models/Song';
|
||||
import Song from '../models/Song';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import Artist from '../models/Artist';
|
||||
import SongRow from '../components/SongRow';
|
||||
import FavSongRow from './FavSongRow';
|
||||
import { LikedSongWithDetails } from '../models/LikedSong';
|
||||
|
||||
const swaToSongCardProps = (song: SongWithArtist) => ({
|
||||
const swaToSongCardProps = (song: Song) => ({
|
||||
songId: song.id,
|
||||
name: song.name,
|
||||
artistName: song.artist.name,
|
||||
artistName: song.artist!.name,
|
||||
cover: song.cover ?? 'https://picsum.photos/200',
|
||||
});
|
||||
|
||||
@@ -41,35 +39,7 @@ const HomeSearchComponent = () => {
|
||||
API.getSearchHistory(0, 12),
|
||||
{ enabled: true }
|
||||
);
|
||||
const songSuggestions = useQuery(API.getSongSuggestions);
|
||||
const songArtistSuggestions = useQueries(
|
||||
songSuggestions.data
|
||||
?.filter((song) => song.artistId !== null)
|
||||
.map(({ artistId }) => API.getArtist(artistId)) ?? []
|
||||
);
|
||||
const isLoadingSuggestions = useMemo(
|
||||
() => songSuggestions.isLoading || songArtistSuggestions.some((q) => q.isLoading),
|
||||
[songSuggestions, songArtistSuggestions]
|
||||
);
|
||||
const suggestionsData = useMemo(() => {
|
||||
if (isLoadingSuggestions) {
|
||||
return [];
|
||||
}
|
||||
return (
|
||||
songSuggestions.data
|
||||
?.map((song): [Song, Artist | undefined] => [
|
||||
song,
|
||||
songArtistSuggestions
|
||||
.map((q) => q.data)
|
||||
.filter((d) => d !== undefined)
|
||||
.find((data) => data?.id === song.artistId),
|
||||
])
|
||||
// We do not need the song
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.filter(([song, artist]) => artist !== undefined)
|
||||
.map(([song, artist]) => ({ ...song, artist: artist! })) ?? []
|
||||
);
|
||||
}, [songSuggestions, songArtistSuggestions]);
|
||||
const songSuggestions = useQuery(API.getSongSuggestions(['artist']));
|
||||
|
||||
return (
|
||||
<VStack mt="5" style={{ overflow: 'hidden' }}>
|
||||
@@ -94,11 +64,11 @@ const HomeSearchComponent = () => {
|
||||
</Card>
|
||||
<Card shadow={3} mt={5} mb={5}>
|
||||
<Heading margin={5}>{translate('songsToGetBetter')}</Heading>
|
||||
{isLoadingSuggestions ? (
|
||||
{!songSuggestions.data ? (
|
||||
<LoadingComponent />
|
||||
) : (
|
||||
<CardGridCustom
|
||||
content={suggestionsData.map(swaToSongCardProps)}
|
||||
content={songSuggestions.data.map(swaToSongCardProps)}
|
||||
cardComponent={SongCard}
|
||||
/>
|
||||
)}
|
||||
@@ -219,19 +189,6 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
const FavoritesComponent = () => {
|
||||
const navigation = useNavigation();
|
||||
const favoritesQuery = useQuery(API.getLikedSongs());
|
||||
const songQueries = useQueries(
|
||||
favoritesQuery.data
|
||||
?.map((favorite) => favorite.songId)
|
||||
.map((songId) => API.getSong(songId)) ?? []
|
||||
);
|
||||
|
||||
const favSongWithDetails = favoritesQuery?.data
|
||||
?.map((favorite) => ({
|
||||
...favorite,
|
||||
details: songQueries.find((query) => query.data?.id == favorite.songId)?.data,
|
||||
}))
|
||||
.filter((favorite) => favorite.details !== undefined)
|
||||
.map((likedSong) => likedSong as LikedSongWithDetails);
|
||||
|
||||
if (favoritesQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
@@ -247,13 +204,14 @@ const FavoritesComponent = () => {
|
||||
{translate('songsFilter')}
|
||||
</Text>
|
||||
<Box>
|
||||
{favSongWithDetails?.map((songData) => (
|
||||
{favoritesQuery.data?.map((songData) => (
|
||||
<FavSongRow
|
||||
key={songData.id}
|
||||
FavSong={songData}
|
||||
song={songData.song}
|
||||
addedDate={songData.addedDate}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(songData.details!.name, 'song'); //todo
|
||||
navigation.navigate('Play', { songId: songData.details!.id });
|
||||
API.createSearchHistoryEntry(songData.song.name, 'song'); //todo
|
||||
navigation.navigate('Play', { songId: songData.song!.id });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { HStack, IconButton, Image, Text } from 'native-base';
|
||||
import Song, { SongWithArtist } from '../models/Song';
|
||||
import Song from '../models/Song';
|
||||
import RowCustom from './RowCustom';
|
||||
import TextButton from './TextButton';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import DurationComponent from './DurationComponent';
|
||||
|
||||
type SongRowProps = {
|
||||
song: Song | SongWithArtist; // TODO: remove Song
|
||||
song: Song;
|
||||
isLiked: boolean;
|
||||
onPress: () => void;
|
||||
handleLike: (state: boolean, songId: number) => Promise<void>;
|
||||
|
||||
@@ -242,6 +242,7 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
|
||||
<Animated.View style={[style, isDisabled ? disableStyle : animatedStyle]}>
|
||||
<Pressable
|
||||
focusable={focusable}
|
||||
tabIndex={focusable ? 0 : -1}
|
||||
disabled={isDisabled}
|
||||
onHoverIn={handleMouseEnter}
|
||||
onPressIn={handlePressIn}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import { View, Image, TouchableOpacity } from 'react-native';
|
||||
import { Divider, Text, ScrollView, Row, useMediaQuery, useTheme } from 'native-base';
|
||||
import { useQuery, useQueries } from '../../Queries';
|
||||
import { useQuery } from '../../Queries';
|
||||
import API from '../../API';
|
||||
import Song from '../../models/Song';
|
||||
import ButtonBase from './ButtonBase';
|
||||
import { Icon } from 'iconsax-react-native';
|
||||
import { LoadingView } from '../Loading';
|
||||
@@ -30,42 +29,36 @@ type ScaffoldDesktopCCProps = {
|
||||
|
||||
// TODO a tester avec un historique de plus de 3 musics différente mdr !!
|
||||
const SongHistory = (props: { quantity: number }) => {
|
||||
const playHistoryQuery = useQuery(API.getUserPlayHistory);
|
||||
const songHistory = useQueries(
|
||||
playHistoryQuery.data?.map(({ songID }) => API.getSong(songID)) ?? []
|
||||
);
|
||||
const history = useQuery(API.getUserPlayHistory);
|
||||
const navigation = useNavigation();
|
||||
|
||||
const musics = songHistory
|
||||
.map((h) => h.data)
|
||||
.filter((data): data is Song => data !== undefined)
|
||||
.filter((song, i, array) => array.map((s) => s.id).findIndex((id) => id == song.id) == i)
|
||||
?.slice(0, props.quantity)
|
||||
.map((song: Song) => (
|
||||
<View
|
||||
key={'short-history-tab' + song.id}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('Play', { songId: song.id })}>
|
||||
<Text numberOfLines={1}>{song.name}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
));
|
||||
|
||||
if (!playHistoryQuery.data || playHistoryQuery.isLoading || !songHistory) {
|
||||
if (!history.data || history.isLoading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
const musics = history.data.map((h) => h.song)?.slice(0, props.quantity);
|
||||
|
||||
return (
|
||||
<View>
|
||||
{musics.length === 0 ? (
|
||||
<Text style={{ paddingHorizontal: 16 }}>{translate('menuNoSongsPlayedYet')}</Text>
|
||||
) : (
|
||||
musics
|
||||
musics.map((song) => (
|
||||
<View
|
||||
key={'short-history-tab' + song.id}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate('Play', { songId: song.id })}
|
||||
>
|
||||
<Text numberOfLines={1}>{song.name}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import API from '../API';
|
||||
import { useQuery } from '../Queries';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
export const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user