From a65ce6595ad62cd059df1ef26c481f28d72867d4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 8 Oct 2023 17:47:40 +0200 Subject: [PATCH 1/7] Add a generic include system and implement it for songs --- back/src/song/song.controller.ts | 80 +++++++++++++++++++++++--------- back/src/song/song.service.ts | 8 +++- back/src/utils/include.ts | 34 ++++++++++++++ 3 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 back/src/utils/include.ts diff --git a/back/src/song/song.controller.ts b/back/src/song/song.controller.ts index 577c85a..e526773 100644 --- a/back/src/song/song.controller.ts +++ b/back/src/song/song.controller.ts @@ -22,18 +22,25 @@ import { SongService } from './song.service'; import { Request } from 'express'; import { Prisma, Song } from '@prisma/client'; import { createReadStream, existsSync } from 'fs'; -import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiProperty, ApiResponse, ApiResponseProperty, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiProperty, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { HistoryService } from 'src/history/history.service'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { FilterQuery } from 'src/utils/filter.pipe'; import { Song as _Song } from 'src/_gen/prisma-class/song'; import { SongHistory } from 'src/_gen/prisma-class/song_history'; - +import { IncludeMap, mapInclude } from 'src/utils/include'; class SongHistoryResult { @ApiProperty() best: number; - @ApiProperty({ type: SongHistory, isArray: true}) + @ApiProperty({ type: SongHistory, isArray: true }) history: SongHistory[]; } @@ -47,6 +54,13 @@ export class SongController { '+albumId', '+genreId', ]; + static includableFileds: IncludeMap = { + artist: true, + album: true, + genre: true, + SongHistory: ({ user }) => ({ where: { userID: user.id } }), + likedByUsers: ({ user }) => ({ where: { userId: user.id } }), + }; constructor( private readonly songService: SongService, @@ -54,9 +68,9 @@ export class SongController { ) {} @Get(':id/midi') - @ApiOperation({ description: "Streams the midi file of the requested song"}) - @ApiNotFoundResponse({ description: "Song not found"}) - @ApiOkResponse({ description: "Returns the midi file succesfully"}) + @ApiOperation({ description: 'Streams the midi file of the requested song' }) + @ApiNotFoundResponse({ description: 'Song not found' }) + @ApiOkResponse({ description: 'Returns the midi file succesfully' }) async getMidi(@Param('id', ParseIntPipe) id: number) { const song = await this.songService.song({ id }); if (!song) throw new NotFoundException('Song not found'); @@ -70,9 +84,11 @@ export class SongController { } @Get(':id/illustration') - @ApiOperation({ description: "Streams the illustration of the requested song"}) - @ApiNotFoundResponse({ description: "Song not found"}) - @ApiOkResponse({ description: "Returns the illustration succesfully"}) + @ApiOperation({ + description: 'Streams the illustration of the requested song', + }) + @ApiNotFoundResponse({ description: 'Song not found' }) + @ApiOkResponse({ description: 'Returns the illustration succesfully' }) async getIllustration(@Param('id', ParseIntPipe) id: number) { const song = await this.songService.song({ id }); if (!song) throw new NotFoundException('Song not found'); @@ -90,9 +106,11 @@ export class SongController { } @Get(':id/musicXml') - @ApiOperation({ description: "Streams the musicXML file of the requested song"}) - @ApiNotFoundResponse({ description: "Song not found"}) - @ApiOkResponse({ description: "Returns the musicXML file succesfully"}) + @ApiOperation({ + description: 'Streams the musicXML file of the requested song', + }) + @ApiNotFoundResponse({ description: 'Song not found' }) + @ApiOkResponse({ description: 'Returns the musicXML file succesfully' }) async getMusicXml(@Param('id', ParseIntPipe) id: number) { const song = await this.songService.song({ id }); if (!song) throw new NotFoundException('Song not found'); @@ -102,7 +120,10 @@ export class SongController { } @Post() - @ApiOperation({description: "register a new song in the database, should not be used by the frontend"}) + @ApiOperation({ + description: + 'register a new song in the database, should not be used by the frontend', + }) async create(@Body() createSongDto: CreateSongDto) { try { return await this.songService.createSong({ @@ -118,7 +139,6 @@ export class SongController { : undefined, }); } catch { - throw new ConflictException( await this.songService.song({ name: createSongDto.name }), ); @@ -126,7 +146,7 @@ export class SongController { } @Delete(':id') - @ApiOperation({ description: "delete a song by id"}) + @ApiOperation({ description: 'delete a song by id' }) async remove(@Param('id', ParseIntPipe) id: number) { try { return await this.songService.deleteSong({ id }); @@ -140,6 +160,7 @@ export class SongController { async findAll( @Req() req: Request, @FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput, + @Query('include') include: string, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number, ): Promise> { @@ -147,16 +168,26 @@ export class SongController { skip, take, where, + include: mapInclude(include, req, SongController.includableFileds), }); return new Plage(ret, req); } @Get(':id') - @ApiOperation({ description: "Get a specific song data"}) - @ApiNotFoundResponse({ description: "Song not found"}) - @ApiOkResponse({ type: _Song, description: "Requested song"}) - async findOne(@Param('id', ParseIntPipe) id: number) { - const res = await this.songService.song({ id }); + @ApiOperation({ description: 'Get a specific song data' }) + @ApiNotFoundResponse({ description: 'Song not found' }) + @ApiOkResponse({ type: _Song, description: 'Requested song' }) + async findOne( + @Req() req: Request, + @Param('id', ParseIntPipe) id: number, + @Query('include') include: string, + ) { + const res = await this.songService.song( + { + id, + }, + mapInclude(include, req, SongController.includableFileds), + ); if (res === null) throw new NotFoundException('Song not found'); return res; @@ -165,8 +196,13 @@ export class SongController { @Get(':id/history') @HttpCode(200) @UseGuards(JwtAuthGuard) - @ApiOperation({ description: "get the history of the connected user on a specific song"}) - @ApiOkResponse({ type: SongHistoryResult, description: "Records of previous games of the user"}) + @ApiOperation({ + description: 'get the history of the connected user on a specific song', + }) + @ApiOkResponse({ + type: SongHistoryResult, + description: 'Records of previous games of the user', + }) @ApiUnauthorizedResponse({ description: 'Invalid token' }) async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) { return this.historyService.getForSong({ diff --git a/back/src/song/song.service.ts b/back/src/song/song.service.ts index de7aa02..80112d3 100644 --- a/back/src/song/song.service.ts +++ b/back/src/song/song.service.ts @@ -9,7 +9,7 @@ export class SongService { async songByArtist(data: number): Promise { return this.prisma.song.findMany({ where: { - artistId: {equals: data}, + artistId: { equals: data }, }, }); } @@ -22,9 +22,11 @@ export class SongService { async song( songWhereUniqueInput: Prisma.SongWhereUniqueInput, + include?: Prisma.SongInclude, ): Promise { return this.prisma.song.findUnique({ where: songWhereUniqueInput, + include, }); } @@ -34,14 +36,16 @@ export class SongService { cursor?: Prisma.SongWhereUniqueInput; where?: Prisma.SongWhereInput; orderBy?: Prisma.SongOrderByWithRelationInput; + include?: Prisma.SongInclude; }): Promise { - const { skip, take, cursor, where, orderBy } = params; + const { skip, take, cursor, where, orderBy, include } = params; return this.prisma.song.findMany({ skip, take, cursor, where, orderBy, + include, }); } diff --git a/back/src/utils/include.ts b/back/src/utils/include.ts new file mode 100644 index 0000000..3cbe0c7 --- /dev/null +++ b/back/src/utils/include.ts @@ -0,0 +1,34 @@ +import { Request } from 'express'; +import { BadRequestException } from '@nestjs/common'; + +export type IncludeMap = { + [key in keyof IncludeType]: + | boolean + | ((ctx: { user: { id: number; username: string } }) => IncludeType[key]); +}; + +export function mapInclude( + include: string | undefined, + req: Request, + fields: IncludeMap, +): IncludeType | undefined { + if (!include) return undefined; + + const ret: IncludeType = {} as IncludeType; + for (const key of include.split(',')) { + const value = + typeof fields[key] === 'function' + ? // @ts-expect-error typescript is dumb, once again + fields[key]({ user: req.user }) + : fields[key]; + if (value === true) include[key] = true; + else if (value !== false) { + throw new BadRequestException( + `Invalid include, ${key} is not valid. Valid includes are: ${Object.keys( + fields, + ).join(', ')}.`, + ); + } + } + return ret; +} From 38bbe56e9bae4544a742030481bfd2dab178bc28 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 8 Oct 2023 18:45:53 +0200 Subject: [PATCH 2/7] Add robot tests --- back/src/artist/artist.controller.ts | 25 ++++++++------ back/src/history/history.controller.ts | 24 ++++++++------ back/src/song/song.controller.ts | 1 + back/src/utils/include.ts | 7 ++-- back/test/robot/songs/songs.robot | 45 +++++++++++++++++++++++++- 5 files changed, 79 insertions(+), 23 deletions(-) diff --git a/back/src/artist/artist.controller.ts b/back/src/artist/artist.controller.ts index 9a3ff8a..92ad1c1 100644 --- a/back/src/artist/artist.controller.ts +++ b/back/src/artist/artist.controller.ts @@ -20,10 +20,15 @@ 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 { + 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 { Artist as _Artist } from 'src/_gen/prisma-class/artist'; @Controller('artist') @ApiTags('artist') @@ -33,7 +38,9 @@ export class ArtistController { constructor(private readonly service: ArtistService) {} @Post() - @ApiOperation({ description: "Register a new artist, should not be used by frontend"}) + @ApiOperation({ + description: 'Register a new artist, should not be used by frontend', + }) async create(@Body() dto: CreateArtistDto) { try { return await this.service.create(dto); @@ -43,7 +50,7 @@ export class ArtistController { } @Delete(':id') - @ApiOperation({ description: "Delete an artist by id"}) + @ApiOperation({ description: 'Delete an artist by id' }) async remove(@Param('id', ParseIntPipe) id: number) { try { return await this.service.delete({ id }); @@ -53,8 +60,8 @@ export class ArtistController { } @Get(':id/illustration') - @ApiOperation({ description: "Get an artist's illustration"}) - @ApiNotFoundResponse({ description: "Artist or illustration not found"}) + @ApiOperation({ description: "Get an artist's illustration" }) + @ApiNotFoundResponse({ description: 'Artist or illustration not found' }) async getIllustration(@Param('id', ParseIntPipe) id: number) { const artist = await this.service.get({ id }); if (!artist) throw new NotFoundException('Artist not found'); @@ -71,7 +78,7 @@ export class ArtistController { } @Get() - @ApiOperation({ description: "Get all artists paginated"}) + @ApiOperation({ description: 'Get all artists paginated' }) @ApiOkResponsePlaginated(_Artist) async findAll( @Req() req: Request, @@ -89,8 +96,8 @@ export class ArtistController { } @Get(':id') - @ApiOperation({ description: "Get an artist by id"}) - @ApiOkResponse({ type: _Artist}) + @ApiOperation({ description: 'Get an artist by id' }) + @ApiOkResponse({ type: _Artist }) async findOne(@Param('id', ParseIntPipe) id: number) { const res = await this.service.get({ id }); diff --git a/back/src/history/history.controller.ts b/back/src/history/history.controller.ts index 1d4db7d..22534ec 100644 --- a/back/src/history/history.controller.ts +++ b/back/src/history/history.controller.ts @@ -10,14 +10,20 @@ import { Request, UseGuards, } from '@nestjs/common'; -import { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { SearchHistory, SongHistory } from '@prisma/client'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { SongHistoryDto } from './dto/SongHistoryDto'; import { HistoryService } from './history.service'; import { SearchHistoryDto } from './dto/SearchHistoryDto'; import { SongHistory as _SongHistory } from 'src/_gen/prisma-class/song_history'; -import { SearchHistory as _SearchHistory} from 'src/_gen/prisma-class/search_history'; +import { SearchHistory as _SearchHistory } from 'src/_gen/prisma-class/search_history'; @Controller('history') @ApiTags('history') @@ -26,9 +32,9 @@ export class HistoryController { @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}) + @ApiOkResponse({ type: _SongHistory, isArray: true }) @ApiUnauthorizedResponse({ description: 'Invalid token' }) async getHistory( @Request() req: any, @@ -40,9 +46,9 @@ export class HistoryController { @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}) + @ApiOkResponse({ type: _SearchHistory, isArray: true }) @ApiUnauthorizedResponse({ description: 'Invalid token' }) async getSearchHistory( @Request() req: any, @@ -54,15 +60,15 @@ export class HistoryController { @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 { return this.historyService.createSongHistoryRecord(record); } @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' }) async createSearchHistory( diff --git a/back/src/song/song.controller.ts b/back/src/song/song.controller.ts index e526773..2553705 100644 --- a/back/src/song/song.controller.ts +++ b/back/src/song/song.controller.ts @@ -174,6 +174,7 @@ export class SongController { } @Get(':id') + @UseGuards(JwtAuthGuard) @ApiOperation({ description: 'Get a specific song data' }) @ApiNotFoundResponse({ description: 'Song not found' }) @ApiOkResponse({ type: _Song, description: 'Requested song' }) diff --git a/back/src/utils/include.ts b/back/src/utils/include.ts index 3cbe0c7..0440fef 100644 --- a/back/src/utils/include.ts +++ b/back/src/utils/include.ts @@ -18,11 +18,10 @@ export function mapInclude( for (const key of include.split(',')) { const value = typeof fields[key] === 'function' - ? // @ts-expect-error typescript is dumb, once again - fields[key]({ user: req.user }) + ? fields[key]({ user: req.user }) : fields[key]; - if (value === true) include[key] = true; - else if (value !== false) { + if (value !== false && value !== undefined) ret[key] = value; + else { throw new BadRequestException( `Invalid include, ${key} is not valid. Valid includes are: ${Object.keys( fields, diff --git a/back/test/robot/songs/songs.robot b/back/test/robot/songs/songs.robot index 8bc7bb6..f35e78d 100644 --- a/back/test/robot/songs/songs.robot +++ b/back/test/robot/songs/songs.robot @@ -3,6 +3,7 @@ Documentation Tests of the /song route. ... Ensures that the songs CRUD works corectly. Resource ../rest.resource +Resource ../auth/auth.resource *** Test Cases *** @@ -133,5 +134,47 @@ Get midi file Integer response status 201 GET /song/${res.body.id}/midi Integer response status 200 - #Output + # Output [Teardown] DELETE /song/${res.body.id} + +Find a song with artist + [Documentation] Create a song and find it with it's artist + &{res2}= POST /artist { "name": "Tghjmk"} + Output + Integer response status 201 + &{res}= POST + ... /song + ... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"} + Output + Integer response status 201 + &{get}= GET /song/${res.body.id}?include=artist + Output + Integer response status 200 + Should Be Equal ${res2.body} ${get.body.artist} + [Teardown] Run Keywords DELETE /song/${res.body.id} + ... AND DELETE /artist/${res2.body.id} + +Find a song with artist and history + [Documentation] Create a song and find it with it's artist + ${userID}= RegisterLogin wowusersfkj + &{res2}= POST /artist { "name": "Tghjmk"} + Output + Integer response status 201 + &{res}= POST + ... /song + ... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"} + Output + Integer response status 201 + &{res3}= POST + ... /history + ... { "songID": ${res.body.id}, "userID": ${userID}, "score": 12, "difficulties": {}, "info": {} } + Output + Integer response status 201 + &{get}= GET /song/${res.body.id}?include=artist,SongHistory + Output + Integer response status 200 + Should Be Equal ${res2.body} ${get.body.artist} + Should Be Equal ${res3.body} ${get.body.SongHistory[0]} + [Teardown] Run Keywords DELETE /auth/me + ... AND DELETE /song/${res.body.id} + ... AND DELETE /artist/${res2.body.id} From be58e932a987906ec997b572e34b6eecca32ccf9 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 8 Oct 2023 18:47:18 +0200 Subject: [PATCH 3/7] Run prettier --- back/src/album/album.controller.ts | 12 +++--- back/src/app.controller.ts | 4 +- back/src/auth/auth.controller.ts | 59 ++++++++++++--------------- back/src/auth/auth.service.ts | 2 +- back/src/lesson/lesson.controller.ts | 2 +- back/src/models/plage.ts | 60 ++++++++++++++++++---------- back/src/search/search.controller.ts | 36 +++++++++++------ back/src/users/users.controller.ts | 13 +++++- back/src/users/users.service.ts | 42 ++++++------------- 9 files changed, 124 insertions(+), 106 deletions(-) diff --git a/back/src/album/album.controller.ts b/back/src/album/album.controller.ts index 3051381..0444082 100644 --- a/back/src/album/album.controller.ts +++ b/back/src/album/album.controller.ts @@ -30,7 +30,9 @@ export class AlbumController { constructor(private readonly albumService: AlbumService) {} @Post() - @ApiOperation({ description: "Register a new album, should not be used by frontend"}) + @ApiOperation({ + description: 'Register a new album, should not be used by frontend', + }) async create(@Body() createAlbumDto: CreateAlbumDto) { try { return await this.albumService.createAlbum({ @@ -47,7 +49,7 @@ export class AlbumController { } @Delete(':id') - @ApiOperation({ description: "Delete an album by id"}) + @ApiOperation({ description: 'Delete an album by id' }) async remove(@Param('id', ParseIntPipe) id: number) { try { return await this.albumService.deleteAlbum({ id }); @@ -58,7 +60,7 @@ export class AlbumController { @Get() @ApiOkResponsePlaginated(_Album) - @ApiOperation({ description: "Get all albums paginated"}) + @ApiOperation({ description: 'Get all albums paginated' }) async findAll( @Req() req: Request, @FilterQuery(AlbumController.filterableFields) @@ -75,8 +77,8 @@ export class AlbumController { } @Get(':id') - @ApiOperation({ description: "Get an album by id"}) - @ApiOkResponse({ type: _Album}) + @ApiOperation({ description: 'Get an album by id' }) + @ApiOkResponse({ type: _Album }) async findOne(@Param('id', ParseIntPipe) id: number) { const res = await this.albumService.album({ id }); diff --git a/back/src/app.controller.ts b/back/src/app.controller.ts index 93301d5..a3a5a89 100644 --- a/back/src/app.controller.ts +++ b/back/src/app.controller.ts @@ -7,7 +7,9 @@ export class AppController { constructor(private readonly appService: AppService) {} @Get() - @ApiOkResponse({ description: 'Return a hello world message, used as a health route' }) + @ApiOkResponse({ + description: 'Return a hello world message, used as a health route', + }) getHello(): string { return this.appService.getHello(); } diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts index 94c37e0..e791d6a 100644 --- a/back/src/auth/auth.controller.ts +++ b/back/src/auth/auth.controller.ts @@ -63,11 +63,14 @@ export class AuthController { @Get('login/google') @UseGuards(AuthGuard('google')) - @ApiOperation({description: 'Redirect to google login page'}) + @ApiOperation({ description: 'Redirect to google login page' }) googleLogin() {} @Get('logged/google') - @ApiOperation({description: 'Redirect to the front page after connecting to the google account'}) + @ApiOperation({ + description: + 'Redirect to the front page after connecting to the google account', + }) @UseGuards(AuthGuard('google')) async googleLoginCallbakc(@Req() req: any) { let user = await this.usersService.user({ googleID: req.user.googleID }); @@ -79,9 +82,11 @@ export class AuthController { } @Post('register') - @ApiOperation({description: 'Register a new user'}) + @ApiOperation({ description: 'Register a new user' }) @ApiConflictResponse({ description: 'Username or email already taken' }) - @ApiOkResponse({ description: 'Successfully registered, email sent to verify' }) + @ApiOkResponse({ + description: 'Successfully registered, email sent to verify', + }) @ApiBadRequestResponse({ description: 'Invalid data or database error' }) async register(@Body() registerDto: RegisterDto): Promise { try { @@ -101,19 +106,21 @@ export class AuthController { @Put('verify') @HttpCode(200) @UseGuards(JwtAuthGuard) - @ApiOperation({description: 'Verify the email of the user'}) + @ApiOperation({ description: 'Verify the email of the user' }) @ApiOkResponse({ description: 'Successfully verified' }) @ApiBadRequestResponse({ description: 'Invalid or expired token' }) - async verify(@Request() req: any, @Query('token') token: string): Promise { - if (await this.authService.verifyMail(req.user.id, token)) - return; - throw new BadRequestException("Invalid token. Expired or invalid."); + async verify( + @Request() req: any, + @Query('token') token: string, + ): Promise { + if (await this.authService.verifyMail(req.user.id, token)) return; + throw new BadRequestException('Invalid token. Expired or invalid.'); } @Put('reverify') @UseGuards(JwtAuthGuard) @HttpCode(200) - @ApiOperation({description: 'Resend the verification email'}) + @ApiOperation({ description: 'Resend the verification email' }) async reverify(@Request() req: any): Promise { const user = await this.usersService.user({ id: req.user.id }); if (!user) throw new BadRequestException('Invalid user'); @@ -277,42 +284,28 @@ export class AuthController { @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully added liked song'}) + @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, - ); + 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'}) + @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, - ); + 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, - ) { - return this.usersService.getLikedSongs(+req.user.id) + getLikedSongs(@Request() req: any) { + return this.usersService.getLikedSongs(+req.user.id); } } diff --git a/back/src/auth/auth.service.ts b/back/src/auth/auth.service.ts index 4989675..fd31dbc 100644 --- a/back/src/auth/auth.service.ts +++ b/back/src/auth/auth.service.ts @@ -79,7 +79,7 @@ export class AuthService { console.log('Password reset token failure', e); return false; } - console.log(verified) + console.log(verified); await this.userService.updateUser({ where: { id: verified.userId }, data: { password: new_password }, diff --git a/back/src/lesson/lesson.controller.ts b/back/src/lesson/lesson.controller.ts index 2f8531b..83098a5 100644 --- a/back/src/lesson/lesson.controller.ts +++ b/back/src/lesson/lesson.controller.ts @@ -18,7 +18,7 @@ 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 { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson'; export class Lesson { @ApiProperty() diff --git a/back/src/models/plage.ts b/back/src/models/plage.ts index d93db5c..1e6823a 100644 --- a/back/src/models/plage.ts +++ b/back/src/models/plage.ts @@ -3,14 +3,28 @@ */ import { Type, applyDecorators } from '@nestjs/common'; -import { ApiExtraModels, ApiOkResponse, ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { + ApiExtraModels, + ApiOkResponse, + ApiProperty, + getSchemaPath, +} from '@nestjs/swagger'; export class PlageMetadata { @ApiProperty() this: string; - @ApiProperty({ type: "string", nullable: true, description: "null if there is no next page, couldn't set it in swagger"}) + @ApiProperty({ + type: 'string', + nullable: true, + description: "null if there is no next page, couldn't set it in swagger", + }) next: string | null; - @ApiProperty({ type: "string", nullable: true, description: "null if there is no previous page, couldn't set it in swagger" }) + @ApiProperty({ + type: 'string', + nullable: true, + description: + "null if there is no previous page, couldn't set it in swagger", + }) previous: string | null; } @@ -55,22 +69,24 @@ export class Plage { } } -export const ApiOkResponsePlaginated = >(dataDto: DataDto) => - applyDecorators( - ApiExtraModels(Plage, dataDto), - ApiOkResponse({ - schema: { - allOf: [ - { $ref: getSchemaPath(Plage) }, - { - properties: { - data: { - type: 'array', - items: { $ref: getSchemaPath(dataDto) }, - }, - }, - }, - ], - }, - }) - ) \ No newline at end of file +export const ApiOkResponsePlaginated = >( + dataDto: DataDto, +) => + applyDecorators( + ApiExtraModels(Plage, dataDto), + ApiOkResponse({ + schema: { + allOf: [ + { $ref: getSchemaPath(Plage) }, + { + properties: { + data: { + type: 'array', + items: { $ref: getSchemaPath(dataDto) }, + }, + }, + }, + ], + }, + }), + ); diff --git a/back/src/search/search.controller.ts b/back/src/search/search.controller.ts index 9c54632..ac5d0f4 100644 --- a/back/src/search/search.controller.ts +++ b/back/src/search/search.controller.ts @@ -12,7 +12,13 @@ import { Request, UseGuards, } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation, ApiParam, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { Artist, Genre, Song } from '@prisma/client'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { SearchSongDto } from './dto/search-song.dto'; @@ -27,9 +33,9 @@ export class SearchController { constructor(private readonly searchService: SearchService) {} @Get('songs/:query') - @ApiOkResponse({ type: _Song, isArray: true}) - @ApiOperation({ description: "Search a song"}) - @ApiUnauthorizedResponse({ description: "Invalid token"}) + @ApiOkResponse({ type: _Song, isArray: true }) + @ApiOperation({ description: 'Search a song' }) + @ApiUnauthorizedResponse({ description: 'Invalid token' }) @UseGuards(JwtAuthGuard) async searchSong( @Request() req: any, @@ -46,10 +52,13 @@ export class SearchController { @Get('genres/:query') @UseGuards(JwtAuthGuard) - @ApiUnauthorizedResponse({ description: "Invalid token"}) - @ApiOkResponse({ type: _Genre, isArray: true}) - @ApiOperation({ description: "Search a genre"}) - async searchGenre(@Request() req: any, @Param('query') query: string): Promise { + @ApiUnauthorizedResponse({ description: 'Invalid token' }) + @ApiOkResponse({ type: _Genre, isArray: true }) + @ApiOperation({ description: 'Search a genre' }) + async searchGenre( + @Request() req: any, + @Param('query') query: string, + ): Promise { try { const ret = await this.searchService.genreByGuess(query, req.user?.id); if (!ret.length) throw new NotFoundException(); @@ -61,10 +70,13 @@ export class SearchController { @Get('artists/:query') @UseGuards(JwtAuthGuard) - @ApiOkResponse({ type: _Artist, isArray: true}) - @ApiUnauthorizedResponse({ description: "Invalid token"}) - @ApiOperation({ description: "Search an artist"}) - async searchArtists(@Request() req: any, @Param('query') query: string): Promise { + @ApiOkResponse({ type: _Artist, isArray: true }) + @ApiUnauthorizedResponse({ description: 'Invalid token' }) + @ApiOperation({ description: 'Search an artist' }) + async searchArtists( + @Request() req: any, + @Param('query') query: string, + ): Promise { try { const ret = await this.searchService.artistByGuess(query, req.user?.id); if (!ret.length) throw new NotFoundException(); diff --git a/back/src/users/users.controller.ts b/back/src/users/users.controller.ts index 65db03d..54347b9 100644 --- a/back/src/users/users.controller.ts +++ b/back/src/users/users.controller.ts @@ -1,4 +1,11 @@ -import { Controller, Get, Post, Param, NotFoundException, Response } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Param, + NotFoundException, + Response, +} from '@nestjs/common'; import { UsersService } from './users.service'; import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { User } from 'src/models/user'; @@ -22,7 +29,9 @@ export class UsersController { } @Get(':id/picture') - @ApiOkResponse({description: 'Return the profile picture of the requested user'}) + @ApiOkResponse({ + description: 'Return the profile picture of the requested user', + }) async getPicture(@Response() res: any, @Param('id') id: number) { return await this.usersService.getProfilePicture(+id, res); } diff --git a/back/src/users/users.service.ts b/back/src/users/users.service.ts index fd8fdcf..f0267ab 100644 --- a/back/src/users/users.service.ts +++ b/back/src/users/users.service.ts @@ -12,9 +12,7 @@ import fetch from 'node-fetch'; @Injectable() export class UsersService { - constructor( - private prisma: PrismaService, - ) {} + constructor(private prisma: PrismaService) {} async user( userWhereUniqueInput: Prisma.UserWhereUniqueInput, @@ -101,35 +99,21 @@ export class UsersService { resp.body!.pipe(res); } - async addLikedSong( - userId: number, - songId: number, - ) { - return this.prisma.likedSongs.create( - { - data: { songId: songId, userId: userId } - } - ) + async addLikedSong(userId: number, songId: number) { + return this.prisma.likedSongs.create({ + data: { songId: songId, userId: userId }, + }); } - async getLikedSongs( - userId: number, - ) { - return this.prisma.likedSongs.findMany( - { - where: { userId: userId }, - } - ) + async getLikedSongs(userId: number) { + return this.prisma.likedSongs.findMany({ + where: { userId: userId }, + }); } - async removeLikedSong( - userId: number, - songId: number, - ) { - return this.prisma.likedSongs.deleteMany( - { - where: { userId: userId, songId: songId }, - } - ) + async removeLikedSong(userId: number, songId: number) { + return this.prisma.likedSongs.deleteMany({ + where: { userId: userId, songId: songId }, + }); } } From 76d7e69d19eb4dbe079dc4f554106cc883a2f927 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 8 Oct 2023 19:15:25 +0200 Subject: [PATCH 4/7] Add includable fields for all ressources --- back/src/album/album.controller.ts | 21 +++++++++++++++++++-- back/src/album/album.service.ts | 6 +++++- back/src/artist/artist.controller.ts | 21 +++++++++++++++++++-- back/src/artist/artist.service.ts | 10 ++++++++-- back/src/genre/genre.controller.ts | 20 ++++++++++++++++++-- back/src/genre/genre.service.ts | 10 ++++++++-- back/src/lesson/lesson.controller.ts | 22 +++++++++++++++++++--- back/src/lesson/lesson.service.ts | 10 ++++++++-- back/src/search/search.controller.ts | 26 +++++++++++++++++++++++--- back/src/search/search.service.ts | 21 ++++++++++++++++++--- back/src/song/song.controller.ts | 9 ++++----- 11 files changed, 149 insertions(+), 27 deletions(-) diff --git a/back/src/album/album.controller.ts b/back/src/album/album.controller.ts index 0444082..0a88e75 100644 --- a/back/src/album/album.controller.ts +++ b/back/src/album/album.controller.ts @@ -12,6 +12,7 @@ import { Post, Query, Req, + UseGuards, } from '@nestjs/common'; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage'; import { CreateAlbumDto } from './dto/create-album.dto'; @@ -21,11 +22,18 @@ 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'; @Controller('album') @ApiTags('album') +@UseGuards(JwtAuthGuard) export class AlbumController { static filterableFields: string[] = ['+id', 'name', '+artistId']; + static includableFields: IncludeMap = { + artist: true, + Song: true, + }; constructor(private readonly albumService: AlbumService) {} @@ -65,6 +73,7 @@ export class AlbumController { @Req() req: Request, @FilterQuery(AlbumController.filterableFields) where: Prisma.AlbumWhereInput, + @Query('include') include: string, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number, ): Promise> { @@ -72,6 +81,7 @@ export class AlbumController { skip, take, where, + include: mapInclude(include, req, AlbumController.includableFields), }); return new Plage(ret, req); } @@ -79,8 +89,15 @@ export class AlbumController { @Get(':id') @ApiOperation({ description: 'Get an album by id' }) @ApiOkResponse({ type: _Album }) - async findOne(@Param('id', ParseIntPipe) id: number) { - const res = await this.albumService.album({ id }); + async findOne( + @Req() req: Request, + @Query('include') include: string, + @Param('id', ParseIntPipe) id: number, + ) { + const res = await this.albumService.album( + { id }, + mapInclude(include, req, AlbumController.includableFields), + ); if (res === null) throw new NotFoundException('Album not found'); return res; diff --git a/back/src/album/album.service.ts b/back/src/album/album.service.ts index e144b32..796707f 100644 --- a/back/src/album/album.service.ts +++ b/back/src/album/album.service.ts @@ -14,9 +14,11 @@ export class AlbumService { async album( albumWhereUniqueInput: Prisma.AlbumWhereUniqueInput, + include?: Prisma.AlbumInclude, ): Promise { return this.prisma.album.findUnique({ where: albumWhereUniqueInput, + include, }); } @@ -26,14 +28,16 @@ export class AlbumService { cursor?: Prisma.AlbumWhereUniqueInput; where?: Prisma.AlbumWhereInput; orderBy?: Prisma.AlbumOrderByWithRelationInput; + include?: Prisma.AlbumInclude; }): Promise { - const { skip, take, cursor, where, orderBy } = params; + const { skip, take, cursor, where, orderBy, include } = params; return this.prisma.album.findMany({ skip, take, cursor, where, orderBy, + include, }); } diff --git a/back/src/artist/artist.controller.ts b/back/src/artist/artist.controller.ts index 92ad1c1..589f476 100644 --- a/back/src/artist/artist.controller.ts +++ b/back/src/artist/artist.controller.ts @@ -14,6 +14,7 @@ import { Query, Req, StreamableFile, + UseGuards, } from '@nestjs/common'; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage'; import { CreateArtistDto } from './dto/create-artist.dto'; @@ -29,11 +30,18 @@ import { 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'; @Controller('artist') @ApiTags('artist') +@UseGuards(JwtAuthGuard) export class ArtistController { static filterableFields = ['+id', 'name']; + static includableFields: IncludeMap = { + Song: true, + Album: true, + }; constructor(private readonly service: ArtistService) {} @@ -84,6 +92,7 @@ export class ArtistController { @Req() req: Request, @FilterQuery(ArtistController.filterableFields) where: Prisma.ArtistWhereInput, + @Query('include') include: string, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number, ): Promise> { @@ -91,6 +100,7 @@ export class ArtistController { skip, take, where, + include: mapInclude(include, req, ArtistController.includableFields), }); return new Plage(ret, req); } @@ -98,8 +108,15 @@ export class ArtistController { @Get(':id') @ApiOperation({ description: 'Get an artist by id' }) @ApiOkResponse({ type: _Artist }) - async findOne(@Param('id', ParseIntPipe) id: number) { - const res = await this.service.get({ id }); + async findOne( + @Req() req: Request, + @Query('include') include: string, + @Param('id', ParseIntPipe) id: number, + ) { + const res = await this.service.get( + { id }, + mapInclude(include, req, ArtistController.includableFields), + ); if (res === null) throw new NotFoundException('Artist not found'); return res; diff --git a/back/src/artist/artist.service.ts b/back/src/artist/artist.service.ts index 9140b86..658ee2c 100644 --- a/back/src/artist/artist.service.ts +++ b/back/src/artist/artist.service.ts @@ -12,9 +12,13 @@ export class ArtistService { }); } - async get(where: Prisma.ArtistWhereUniqueInput): Promise { + async get( + where: Prisma.ArtistWhereUniqueInput, + include?: Prisma.ArtistInclude, + ): Promise { return this.prisma.artist.findUnique({ where, + include, }); } @@ -24,14 +28,16 @@ export class ArtistService { cursor?: Prisma.ArtistWhereUniqueInput; where?: Prisma.ArtistWhereInput; orderBy?: Prisma.ArtistOrderByWithRelationInput; + include?: Prisma.ArtistInclude; }): Promise { - const { skip, take, cursor, where, orderBy } = params; + const { skip, take, cursor, where, orderBy, include } = params; return this.prisma.artist.findMany({ skip, take, cursor, where, orderBy, + include, }); } diff --git a/back/src/genre/genre.controller.ts b/back/src/genre/genre.controller.ts index 0ebf2af..d7f902b 100644 --- a/back/src/genre/genre.controller.ts +++ b/back/src/genre/genre.controller.ts @@ -13,6 +13,7 @@ import { Query, Req, StreamableFile, + UseGuards, } from '@nestjs/common'; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage'; import { CreateGenreDto } from './dto/create-genre.dto'; @@ -23,11 +24,17 @@ 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'; @Controller('genre') @ApiTags('genre') +@UseGuards(JwtAuthGuard) export class GenreController { static filterableFields: string[] = ['+id', 'name']; + static includableFields: IncludeMap = { + Song: true, + }; constructor(private readonly service: GenreService) {} @@ -71,6 +78,7 @@ 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, ): Promise> { @@ -78,13 +86,21 @@ export class GenreController { skip, take, where, + include: mapInclude(include, req, GenreController.includableFields), }); return new Plage(ret, req); } @Get(':id') - async findOne(@Param('id', ParseIntPipe) id: number) { - const res = await this.service.get({ id }); + async findOne( + @Req() req: Request, + @Query('include') include: string, + @Param('id', ParseIntPipe) id: number, + ) { + const res = await this.service.get( + { id }, + mapInclude(include, req, GenreController.includableFields), + ); if (res === null) throw new NotFoundException('Genre not found'); return res; diff --git a/back/src/genre/genre.service.ts b/back/src/genre/genre.service.ts index 75cace2..ae6aab3 100644 --- a/back/src/genre/genre.service.ts +++ b/back/src/genre/genre.service.ts @@ -12,9 +12,13 @@ export class GenreService { }); } - async get(where: Prisma.GenreWhereUniqueInput): Promise { + async get( + where: Prisma.GenreWhereUniqueInput, + include?: Prisma.GenreInclude, + ): Promise { return this.prisma.genre.findUnique({ where, + include, }); } @@ -24,14 +28,16 @@ export class GenreService { cursor?: Prisma.GenreWhereUniqueInput; where?: Prisma.GenreWhereInput; orderBy?: Prisma.GenreOrderByWithRelationInput; + include?: Prisma.GenreInclude; }): Promise { - const { skip, take, cursor, where, orderBy } = params; + const { skip, take, cursor, where, orderBy, include } = params; return this.prisma.genre.findMany({ skip, take, cursor, where, orderBy, + include, }); } diff --git a/back/src/lesson/lesson.controller.ts b/back/src/lesson/lesson.controller.ts index 83098a5..1c692e9 100644 --- a/back/src/lesson/lesson.controller.ts +++ b/back/src/lesson/lesson.controller.ts @@ -3,7 +3,6 @@ import { Get, Query, Req, - Request, Param, ParseIntPipe, DefaultValuePipe, @@ -12,6 +11,7 @@ import { Body, Delete, NotFoundException, + UseGuards, } from '@nestjs/common'; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage'; import { LessonService } from './lesson.service'; @@ -19,6 +19,9 @@ import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; import { Prisma, Skill } from '@prisma/client'; import { FilterQuery } from 'src/utils/filter.pipe'; import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson'; +import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; +import { IncludeMap, mapInclude } from 'src/utils/include'; +import { Request } from 'express'; export class Lesson { @ApiProperty() @@ -35,6 +38,7 @@ export class Lesson { @ApiTags('lessons') @Controller('lesson') +@UseGuards(JwtAuthGuard) export class LessonController { static filterableFields: string[] = [ '+id', @@ -42,6 +46,9 @@ export class LessonController { '+requiredLevel', 'mainSkill', ]; + static includableFields: IncludeMap = { + LessonHistory: true, + }; constructor(private lessonService: LessonService) {} @@ -54,6 +61,7 @@ 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, ): Promise> { @@ -61,6 +69,7 @@ export class LessonController { skip, take, where, + include: mapInclude(include, request, LessonController.includableFields), }); return new Plage(ret, request); } @@ -69,8 +78,15 @@ export class LessonController { summary: 'Get a particular lessons', }) @Get(':id') - async get(@Param('id', ParseIntPipe) id: number): Promise { - const ret = await this.lessonService.get(id); + async get( + @Req() req: Request, + @Query('include') include: string, + @Param('id', ParseIntPipe) id: number, + ): Promise { + const ret = await this.lessonService.get( + id, + mapInclude(include, req, LessonController.includableFields), + ); if (!ret) throw new NotFoundException(); return ret; } diff --git a/back/src/lesson/lesson.service.ts b/back/src/lesson/lesson.service.ts index aed4a1e..9a1e94e 100644 --- a/back/src/lesson/lesson.service.ts +++ b/back/src/lesson/lesson.service.ts @@ -12,22 +12,28 @@ export class LessonService { cursor?: Prisma.LessonWhereUniqueInput; where?: Prisma.LessonWhereInput; orderBy?: Prisma.LessonOrderByWithRelationInput; + include?: Prisma.LessonInclude; }): Promise { - const { skip, take, cursor, where, orderBy } = params; + const { skip, take, cursor, where, orderBy, include } = params; return this.prisma.lesson.findMany({ skip, take, cursor, where, orderBy, + include, }); } - async get(id: number): Promise { + async get( + id: number, + include?: Prisma.LessonInclude, + ): Promise { return this.prisma.lesson.findFirst({ where: { id: id, }, + include, }); } diff --git a/back/src/search/search.controller.ts b/back/src/search/search.controller.ts index ac5d0f4..a6fad32 100644 --- a/back/src/search/search.controller.ts +++ b/back/src/search/search.controller.ts @@ -9,6 +9,7 @@ import { Param, ParseIntPipe, Post, + Query, Request, UseGuards, } from '@nestjs/common'; @@ -26,6 +27,10 @@ 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') @@ -39,10 +44,15 @@ export class SearchController { @UseGuards(JwtAuthGuard) async searchSong( @Request() req: any, + @Query('include') include: string, @Param('query') query: string, ): Promise { try { - const ret = await this.searchService.songByGuess(query, req.user?.id); + const ret = await this.searchService.songByGuess( + query, + req.user?.id, + mapInclude(include, req, SongController.includableFields), + ); if (!ret.length) throw new NotFoundException(); else return ret; } catch (error) { @@ -57,10 +67,15 @@ export class SearchController { @ApiOperation({ description: 'Search a genre' }) async searchGenre( @Request() req: any, + @Query('include') include: string, @Param('query') query: string, ): Promise { try { - const ret = await this.searchService.genreByGuess(query, req.user?.id); + const ret = await this.searchService.genreByGuess( + query, + req.user?.id, + mapInclude(include, req, GenreController.includableFields), + ); if (!ret.length) throw new NotFoundException(); else return ret; } catch (error) { @@ -75,10 +90,15 @@ export class SearchController { @ApiOperation({ description: 'Search an artist' }) async searchArtists( @Request() req: any, + @Query('include') include: string, @Param('query') query: string, ): Promise { try { - const ret = await this.searchService.artistByGuess(query, req.user?.id); + const ret = await this.searchService.artistByGuess( + query, + req.user?.id, + mapInclude(include, req, ArtistController.includableFields), + ); if (!ret.length) throw new NotFoundException(); else return ret; } catch (error) { diff --git a/back/src/search/search.service.ts b/back/src/search/search.service.ts index fd0ad5d..5175831 100644 --- a/back/src/search/search.service.ts +++ b/back/src/search/search.service.ts @@ -10,27 +10,42 @@ export class SearchService { private history: HistoryService, ) {} - async songByGuess(query: string, userID: number): Promise { + async songByGuess( + query: string, + userID: number, + include?: Prisma.SongInclude, + ): Promise { return this.prisma.song.findMany({ where: { name: { contains: query, mode: 'insensitive' }, }, + include, }); } - async genreByGuess(query: string, userID: number): Promise { + async genreByGuess( + query: string, + userID: number, + include?: Prisma.GenreInclude, + ): Promise { return this.prisma.genre.findMany({ where: { name: { contains: query, mode: 'insensitive' }, }, + include, }); } - async artistByGuess(query: string, userID: number): Promise { + async artistByGuess( + query: string, + userID: number, + include?: Prisma.ArtistInclude, + ): Promise { return this.prisma.artist.findMany({ where: { name: { contains: query, mode: 'insensitive' }, }, + include, }); } } diff --git a/back/src/song/song.controller.ts b/back/src/song/song.controller.ts index 2553705..38269e9 100644 --- a/back/src/song/song.controller.ts +++ b/back/src/song/song.controller.ts @@ -46,6 +46,7 @@ class SongHistoryResult { @Controller('song') @ApiTags('song') +@UseGuards(JwtAuthGuard) export class SongController { static filterableFields: string[] = [ '+id', @@ -54,7 +55,7 @@ export class SongController { '+albumId', '+genreId', ]; - static includableFileds: IncludeMap = { + static includableFields: IncludeMap = { artist: true, album: true, genre: true, @@ -168,13 +169,12 @@ export class SongController { skip, take, where, - include: mapInclude(include, req, SongController.includableFileds), + include: mapInclude(include, req, SongController.includableFields), }); return new Plage(ret, req); } @Get(':id') - @UseGuards(JwtAuthGuard) @ApiOperation({ description: 'Get a specific song data' }) @ApiNotFoundResponse({ description: 'Song not found' }) @ApiOkResponse({ type: _Song, description: 'Requested song' }) @@ -187,7 +187,7 @@ export class SongController { { id, }, - mapInclude(include, req, SongController.includableFileds), + mapInclude(include, req, SongController.includableFields), ); if (res === null) throw new NotFoundException('Song not found'); @@ -196,7 +196,6 @@ export class SongController { @Get(':id/history') @HttpCode(200) - @UseGuards(JwtAuthGuard) @ApiOperation({ description: 'get the history of the connected user on a specific song', }) From 8d8323e3822516bb8d0a455d6af8f4b5c6c4450e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 8 Oct 2023 19:16:37 +0200 Subject: [PATCH 5/7] Cleanup inports --- back/src/album/album.controller.ts | 1 - back/src/artist/artist.controller.ts | 3 +-- back/src/auth/auth.controller.ts | 3 --- back/src/search/search.controller.ts | 7 ------- back/src/search/search.service.ts | 2 +- 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/back/src/album/album.controller.ts b/back/src/album/album.controller.ts index 0a88e75..70f0120 100644 --- a/back/src/album/album.controller.ts +++ b/back/src/album/album.controller.ts @@ -1,5 +1,4 @@ import { - BadRequestException, Body, ConflictException, Controller, diff --git a/back/src/artist/artist.controller.ts b/back/src/artist/artist.controller.ts index 589f476..67707e0 100644 --- a/back/src/artist/artist.controller.ts +++ b/back/src/artist/artist.controller.ts @@ -1,5 +1,4 @@ import { - BadRequestException, Body, ConflictException, Controller, @@ -14,7 +13,7 @@ import { Query, Req, StreamableFile, - UseGuards, + UseGuards, } from '@nestjs/common'; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage'; import { CreateArtistDto } from './dto/create-artist.dto'; diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts index e791d6a..4e613e7 100644 --- a/back/src/auth/auth.controller.ts +++ b/back/src/auth/auth.controller.ts @@ -32,11 +32,8 @@ import { ApiBearerAuth, ApiBody, ApiConflictResponse, - ApiCreatedResponse, - ApiNoContentResponse, ApiOkResponse, ApiOperation, - ApiResponse, ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; diff --git a/back/src/search/search.controller.ts b/back/src/search/search.controller.ts index a6fad32..fa5f4db 100644 --- a/back/src/search/search.controller.ts +++ b/back/src/search/search.controller.ts @@ -1,14 +1,9 @@ import { - BadRequestException, - Body, Controller, Get, - HttpCode, InternalServerErrorException, NotFoundException, Param, - ParseIntPipe, - Post, Query, Request, UseGuards, @@ -16,13 +11,11 @@ import { import { ApiOkResponse, ApiOperation, - ApiParam, ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { Artist, Genre, Song } from '@prisma/client'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; -import { SearchSongDto } from './dto/search-song.dto'; import { SearchService } from './search.service'; import { Song as _Song } from 'src/_gen/prisma-class/song'; import { Genre as _Genre } from 'src/_gen/prisma-class/genre'; diff --git a/back/src/search/search.service.ts b/back/src/search/search.service.ts index 5175831..c983018 100644 --- a/back/src/search/search.service.ts +++ b/back/src/search/search.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Album, Artist, Prisma, Song, Genre } from '@prisma/client'; +import { Artist, Prisma, Song, Genre } from '@prisma/client'; import { HistoryService } from 'src/history/history.service'; import { PrismaService } from 'src/prisma/prisma.service'; From a92ca75760ed8e6904b1ad46fe42b58121fae515 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 12 Oct 2023 12:47:29 +0200 Subject: [PATCH 6/7] Fix dev nginx --- docker-compose.dev.yml | 6 +++--- front/API.ts | 4 +--- front/nginx.conf.template.dev | 3 ++- shell.nix | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8a9e635..f0b4d8b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -70,9 +70,9 @@ services: nginx: image: nginx environment: - - API_URL=http://back:3000 - - SCOROMETER_URL=http://scorometer:6543 - - FRONT_URL=http://front:19006 + - API_URL=${API_URL:-http://back:3000} + - SCOROMETER_URL=${SCOROMETER_URL:-http://scorometer:6543} + - FRONT_URL=${FRONT_URL:-http://front:19006} - PORT=4567 depends_on: - back diff --git a/front/API.ts b/front/API.ts index d4cb899..5254500 100644 --- a/front/API.ts +++ b/front/API.ts @@ -66,9 +66,7 @@ export class ValidationError extends Error { export default class API { public static readonly baseUrl = - process.env.NODE_ENV != 'development' && Platform.OS === 'web' - ? '/api' - : process.env.EXPO_PUBLIC_API_URL!; + Platform.OS === 'web' ? '/api' : process.env.EXPO_PUBLIC_API_URL!; public static async fetch( params: FetchParams, handle: Pick, 'raw'> diff --git a/front/nginx.conf.template.dev b/front/nginx.conf.template.dev index b7caf3b..66aba7b 100644 --- a/front/nginx.conf.template.dev +++ b/front/nginx.conf.template.dev @@ -21,7 +21,8 @@ server { proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; - proxy_pass ${SCOROMETER_URL}; + set $upstream ${SCOROMETER_URL}; + proxy_pass $upstream; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; proxy_http_version 1.1; diff --git a/shell.nix b/shell.nix index 6aa53b8..0fa4181 100644 --- a/shell.nix +++ b/shell.nix @@ -8,7 +8,7 @@ pkgs.mkShell { eslint_d nodejs_16 yarn - (python3.withPackages (ps: with ps; [requests])) + (python3.withPackages (ps: with ps; [requests mido])) pkg-config ]; shellHook = with pkgs; '' From bfb6cf5958b5b54885f09a3a5f2b84a9bee34efd Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 12 Oct 2023 13:29:57 +0200 Subject: [PATCH 7/7] Disable jwt auth for images routes --- back/src/artist/artist.controller.ts | 2 ++ back/src/auth/jwt-auth.guard.ts | 23 +++++++++++++++++++++-- back/src/auth/public.ts | 4 ++++ back/src/genre/genre.controller.ts | 2 ++ back/src/song/song.controller.ts | 2 ++ 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 back/src/auth/public.ts diff --git a/back/src/artist/artist.controller.ts b/back/src/artist/artist.controller.ts index 67707e0..eec918e 100644 --- a/back/src/artist/artist.controller.ts +++ b/back/src/artist/artist.controller.ts @@ -31,6 +31,7 @@ import { FilterQuery } from 'src/utils/filter.pipe'; import { Artist as _Artist } from 'src/_gen/prisma-class/artist'; import { IncludeMap, mapInclude } from 'src/utils/include'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; +import { Public } from 'src/auth/public'; @Controller('artist') @ApiTags('artist') @@ -69,6 +70,7 @@ export class ArtistController { @Get(':id/illustration') @ApiOperation({ description: "Get an artist's illustration" }) @ApiNotFoundResponse({ description: 'Artist or illustration not found' }) + @Public() async getIllustration(@Param('id', ParseIntPipe) id: number) { const artist = await this.service.get({ id }); if (!artist) throw new NotFoundException('Artist not found'); diff --git a/back/src/auth/jwt-auth.guard.ts b/back/src/auth/jwt-auth.guard.ts index 2155290..b1cd372 100644 --- a/back/src/auth/jwt-auth.guard.ts +++ b/back/src/auth/jwt-auth.guard.ts @@ -1,5 +1,24 @@ -import { Injectable } from '@nestjs/common'; +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { IS_PUBLIC_KEY } from './public'; @Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') {} +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + console.log(context); + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + console.log(isPublic); + if (isPublic) { + return true; + } + return super.canActivate(context); + } +} diff --git a/back/src/auth/public.ts b/back/src/auth/public.ts new file mode 100644 index 0000000..b3845e1 --- /dev/null +++ b/back/src/auth/public.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/back/src/genre/genre.controller.ts b/back/src/genre/genre.controller.ts index d7f902b..9039070 100644 --- a/back/src/genre/genre.controller.ts +++ b/back/src/genre/genre.controller.ts @@ -26,6 +26,7 @@ import { FilterQuery } from 'src/utils/filter.pipe'; import { Genre as _Genre } from 'src/_gen/prisma-class/genre'; import { IncludeMap, mapInclude } from 'src/utils/include'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; +import { Public } from 'src/auth/public'; @Controller('genre') @ApiTags('genre') @@ -57,6 +58,7 @@ export class GenreController { } @Get(':id/illustration') + @Public() async getIllustration(@Param('id', ParseIntPipe) id: number) { const genre = await this.service.get({ id }); if (!genre) throw new NotFoundException('Genre not found'); diff --git a/back/src/song/song.controller.ts b/back/src/song/song.controller.ts index 38269e9..e2143cc 100644 --- a/back/src/song/song.controller.ts +++ b/back/src/song/song.controller.ts @@ -36,6 +36,7 @@ import { FilterQuery } from 'src/utils/filter.pipe'; import { Song as _Song } from 'src/_gen/prisma-class/song'; import { SongHistory } from 'src/_gen/prisma-class/song_history'; import { IncludeMap, mapInclude } from 'src/utils/include'; +import { Public } from 'src/auth/public'; class SongHistoryResult { @ApiProperty() @@ -90,6 +91,7 @@ export class SongController { }) @ApiNotFoundResponse({ description: 'Song not found' }) @ApiOkResponse({ description: 'Returns the illustration succesfully' }) + @Public() async getIllustration(@Param('id', ParseIntPipe) id: number) { const song = await this.songService.song({ id }); if (!song) throw new NotFoundException('Song not found');