Use includes on liked, music, score, search and fav pages
This commit is contained in:
@@ -21,12 +21,12 @@ import {
|
|||||||
Response,
|
Response,
|
||||||
Query,
|
Query,
|
||||||
Param,
|
Param,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from "./auth.service";
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from "./jwt-auth.guard";
|
||||||
import { LocalAuthGuard } from './local-auth.guard';
|
import { LocalAuthGuard } from "./local-auth.guard";
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from "./dto/register.dto";
|
||||||
import { UsersService } from 'src/users/users.service';
|
import { UsersService } from "src/users/users.service";
|
||||||
import {
|
import {
|
||||||
ApiBadRequestResponse,
|
ApiBadRequestResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
@@ -36,39 +36,41 @@ import {
|
|||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { User } from '../models/user';
|
import { User } from "../models/user";
|
||||||
import { JwtToken } from './models/jwt';
|
import { JwtToken } from "./models/jwt";
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from "./dto/login.dto";
|
||||||
import { Profile } from './dto/profile.dto';
|
import { Profile } from "./dto/profile.dto";
|
||||||
import { Setting } from 'src/models/setting';
|
import { Setting } from "src/models/setting";
|
||||||
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
|
import { UpdateSettingDto } from "src/settings/dto/update-setting.dto";
|
||||||
import { SettingsService } from 'src/settings/settings.service';
|
import { SettingsService } from "src/settings/settings.service";
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
import { writeFile } from 'fs';
|
import { writeFile } from "fs";
|
||||||
import { PasswordResetDto } from './dto/password_reset.dto ';
|
import { PasswordResetDto } from "./dto/password_reset.dto ";
|
||||||
|
import { mapInclude } from "src/utils/include";
|
||||||
|
import { SongController } from "src/song/song.controller";
|
||||||
|
|
||||||
@ApiTags('auth')
|
@ApiTags("auth")
|
||||||
@Controller('auth')
|
@Controller("auth")
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Get('login/google')
|
@Get("login/google")
|
||||||
@UseGuards(AuthGuard('google'))
|
@UseGuards(AuthGuard("google"))
|
||||||
@ApiOperation({ description: 'Redirect to google login page' })
|
@ApiOperation({ description: "Redirect to google login page" })
|
||||||
googleLogin() {}
|
googleLogin() { }
|
||||||
|
|
||||||
@Get('logged/google')
|
@Get("logged/google")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description:
|
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) {
|
async googleLoginCallbakc(@Req() req: any) {
|
||||||
let user = await this.usersService.user({ googleID: req.user.googleID });
|
let user = await this.usersService.user({ googleID: req.user.googleID });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -78,13 +80,13 @@ export class AuthController {
|
|||||||
return this.authService.login(user);
|
return this.authService.login(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('register')
|
@Post("register")
|
||||||
@ApiOperation({ description: 'Register a new user' })
|
@ApiOperation({ description: "Register a new user" })
|
||||||
@ApiConflictResponse({ description: 'Username or email already taken' })
|
@ApiConflictResponse({ description: "Username or email already taken" })
|
||||||
@ApiOkResponse({
|
@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> {
|
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const user = await this.usersService.createUser(registerDto);
|
const user = await this.usersService.createUser(registerDto);
|
||||||
@@ -92,73 +94,73 @@ export class AuthController {
|
|||||||
await this.authService.sendVerifyMail(user);
|
await this.authService.sendVerifyMail(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// check if the error is a duplicate key error
|
// check if the error is a duplicate key error
|
||||||
if (e.code === 'P2002') {
|
if (e.code === "P2002") {
|
||||||
throw new ConflictException('Username or email already taken');
|
throw new ConflictException("Username or email already taken");
|
||||||
}
|
}
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('verify')
|
@Put("verify")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOperation({ description: 'Verify the email of the user' })
|
@ApiOperation({ description: "Verify the email of the user" })
|
||||||
@ApiOkResponse({ description: 'Successfully verified' })
|
@ApiOkResponse({ description: "Successfully verified" })
|
||||||
@ApiBadRequestResponse({ description: 'Invalid or expired token' })
|
@ApiBadRequestResponse({ description: "Invalid or expired token" })
|
||||||
async verify(
|
async verify(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('token') token: string,
|
@Query("token") token: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (await this.authService.verifyMail(req.user.id, token)) return;
|
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)
|
@UseGuards(JwtAuthGuard)
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ApiOperation({ description: 'Resend the verification email' })
|
@ApiOperation({ description: "Resend the verification email" })
|
||||||
async reverify(@Request() req: any): Promise<void> {
|
async reverify(@Request() req: any): Promise<void> {
|
||||||
const user = await this.usersService.user({ id: req.user.id });
|
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);
|
await this.authService.sendVerifyMail(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@Put('password-reset')
|
@Put("password-reset")
|
||||||
async password_reset(
|
async password_reset(
|
||||||
@Body() resetDto: PasswordResetDto,
|
@Body() resetDto: PasswordResetDto,
|
||||||
@Query('token') token: string,
|
@Query("token") token: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (await this.authService.changePassword(resetDto.password, token)) return;
|
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)
|
@HttpCode(200)
|
||||||
@Put('forgot-password')
|
@Put("forgot-password")
|
||||||
async forgot_password(@Query('email') email: string): Promise<void> {
|
async forgot_password(@Query("email") email: string): Promise<void> {
|
||||||
console.log(email);
|
console.log(email);
|
||||||
const user = await this.usersService.user({ email: 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);
|
await this.authService.sendPasswordResetMail(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post("login")
|
||||||
@ApiBody({ type: LoginDto })
|
@ApiBody({ type: LoginDto })
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(LocalAuthGuard)
|
@UseGuards(LocalAuthGuard)
|
||||||
@ApiBody({ type: LoginDto })
|
@ApiBody({ type: LoginDto })
|
||||||
@ApiOperation({ description: 'Login with username and password' })
|
@ApiOperation({ description: "Login with username and password" })
|
||||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid credentials' })
|
@ApiUnauthorizedResponse({ description: "Invalid credentials" })
|
||||||
async login(@Request() req: any): Promise<JwtToken> {
|
async login(@Request() req: any): Promise<JwtToken> {
|
||||||
return this.authService.login(req.user);
|
return this.authService.login(req.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('guest')
|
@Post("guest")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ApiOperation({ description: 'Login as a guest account' })
|
@ApiOperation({ description: "Login as a guest account" })
|
||||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||||
async guest(): Promise<JwtToken> {
|
async guest(): Promise<JwtToken> {
|
||||||
const user = await this.usersService.createGuest();
|
const user = await this.usersService.createGuest();
|
||||||
await this.settingsService.createUserSetting(user.id);
|
await this.settingsService.createUserSetting(user.id);
|
||||||
@@ -167,27 +169,27 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ description: 'Get the profile picture of connected user' })
|
@ApiOperation({ description: "Get the profile picture of connected user" })
|
||||||
@ApiOkResponse({ description: 'The user profile picture' })
|
@ApiOkResponse({ description: "The user profile picture" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me/picture')
|
@Get("me/picture")
|
||||||
async getProfilePicture(@Request() req: any, @Response() res: any) {
|
async getProfilePicture(@Request() req: any, @Response() res: any) {
|
||||||
return await this.usersService.getProfilePicture(req.user.id, res);
|
return await this.usersService.getProfilePicture(req.user.id, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'The user profile picture' })
|
@ApiOkResponse({ description: "The user profile picture" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Post('me/picture')
|
@Post("me/picture")
|
||||||
@ApiOperation({ description: 'Upload a new profile picture' })
|
@ApiOperation({ description: "Upload a new profile picture" })
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor("file"))
|
||||||
async postProfilePicture(
|
async postProfilePicture(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@UploadedFile(
|
@UploadedFile(
|
||||||
new ParseFilePipeBuilder()
|
new ParseFilePipeBuilder()
|
||||||
.addFileTypeValidator({
|
.addFileTypeValidator({
|
||||||
fileType: 'jpeg',
|
fileType: "jpeg",
|
||||||
})
|
})
|
||||||
.build({
|
.build({
|
||||||
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
@@ -203,10 +205,10 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully logged in', type: User })
|
@ApiOkResponse({ description: "Successfully logged in", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me')
|
@Get("me")
|
||||||
@ApiOperation({ description: 'Get the user info of connected user' })
|
@ApiOperation({ description: "Get the user info of connected user" })
|
||||||
async getProfile(@Request() req: any): Promise<User> {
|
async getProfile(@Request() req: any): Promise<User> {
|
||||||
const user = await this.usersService.user({ id: req.user.id });
|
const user = await this.usersService.user({ id: req.user.id });
|
||||||
if (!user) throw new InternalServerErrorException();
|
if (!user) throw new InternalServerErrorException();
|
||||||
@@ -215,10 +217,10 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully edited profile', type: User })
|
@ApiOkResponse({ description: "Successfully edited profile", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Put('me')
|
@Put("me")
|
||||||
@ApiOperation({ description: 'Edit the profile of connected user' })
|
@ApiOperation({ description: "Edit the profile of connected user" })
|
||||||
editProfile(
|
editProfile(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() profile: Partial<Profile>,
|
@Body() profile: Partial<Profile>,
|
||||||
@@ -241,20 +243,20 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully deleted', type: User })
|
@ApiOkResponse({ description: "Successfully deleted", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Delete('me')
|
@Delete("me")
|
||||||
@ApiOperation({ description: 'Delete the profile of connected user' })
|
@ApiOperation({ description: "Delete the profile of connected user" })
|
||||||
deleteSelf(@Request() req: any): Promise<User> {
|
deleteSelf(@Request() req: any): Promise<User> {
|
||||||
return this.usersService.deleteUser({ id: req.user.id });
|
return this.usersService.deleteUser({ id: req.user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Patch('me/settings')
|
@Patch("me/settings")
|
||||||
@ApiOperation({ description: 'Edit the settings of connected user' })
|
@ApiOperation({ description: "Edit the settings of connected user" })
|
||||||
udpateSettings(
|
udpateSettings(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() settingUserDto: UpdateSettingDto,
|
@Body() settingUserDto: UpdateSettingDto,
|
||||||
@@ -267,10 +269,10 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me/settings')
|
@Get("me/settings")
|
||||||
@ApiOperation({ description: 'Get the settings of connected user' })
|
@ApiOperation({ description: "Get the settings of connected user" })
|
||||||
async getSettings(@Request() req: any): Promise<Setting> {
|
async getSettings(@Request() req: any): Promise<Setting> {
|
||||||
const result = await this.settingsService.getUserSetting({
|
const result = await this.settingsService.getUserSetting({
|
||||||
userId: +req.user.id,
|
userId: +req.user.id,
|
||||||
@@ -281,28 +283,31 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully added liked song' })
|
@ApiOkResponse({ description: "Successfully added liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Post('me/likes/:id')
|
@Post("me/likes/:id")
|
||||||
addLikedSong(@Request() req: any, @Param('id') songId: number) {
|
addLikedSong(@Request() req: any, @Param("id") songId: number) {
|
||||||
return this.usersService.addLikedSong(+req.user.id, +songId);
|
return this.usersService.addLikedSong(+req.user.id, +songId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully removed liked song' })
|
@ApiOkResponse({ description: "Successfully removed liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Delete('me/likes/:id')
|
@Delete("me/likes/:id")
|
||||||
removeLikedSong(@Request() req: any, @Param('id') songId: number) {
|
removeLikedSong(@Request() req: any, @Param("id") songId: number) {
|
||||||
return this.usersService.removeLikedSong(+req.user.id, +songId);
|
return this.usersService.removeLikedSong(+req.user.id, +songId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully retrieved liked song' })
|
@ApiOkResponse({ description: "Successfully retrieved liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me/likes')
|
@Get("me/likes")
|
||||||
getLikedSongs(@Request() req: any) {
|
getLikedSongs(@Request() req: any, @Query("include") include: string) {
|
||||||
return this.usersService.getLikedSongs(+req.user.id);
|
return this.usersService.getLikedSongs(
|
||||||
|
+req.user.id,
|
||||||
|
mapInclude(include, req, SongController.includableFields),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { User, Prisma } from '@prisma/client';
|
import { User, Prisma } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from "bcryptjs";
|
||||||
import { createHash, randomUUID } from 'crypto';
|
import { createHash, randomUUID } from "crypto";
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { createReadStream, existsSync } from "fs";
|
||||||
import fetch from 'node-fetch';
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) { }
|
||||||
|
|
||||||
async user(
|
async user(
|
||||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
|
||||||
@@ -53,7 +53,7 @@ export class UsersService {
|
|||||||
isGuest: true,
|
isGuest: true,
|
||||||
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
|
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
|
||||||
email: null,
|
email: null,
|
||||||
password: '',
|
password: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ export class UsersService {
|
|||||||
data: Prisma.UserUpdateInput;
|
data: Prisma.UserUpdateInput;
|
||||||
}): Promise<User> {
|
}): Promise<User> {
|
||||||
const { where, data } = params;
|
const { where, data } = params;
|
||||||
if (typeof data.password === 'string')
|
if (typeof data.password === "string")
|
||||||
data.password = await bcrypt.hash(data.password, 8);
|
data.password = await bcrypt.hash(data.password, 8);
|
||||||
else if (data.password && data.password.set)
|
else if (data.password && data.password.set)
|
||||||
data.password = await bcrypt.hash(data.password.set, 8);
|
data.password = await bcrypt.hash(data.password.set, 8);
|
||||||
@@ -89,9 +89,9 @@ export class UsersService {
|
|||||||
const user = await this.user({ id: userId });
|
const user = await this.user({ id: userId });
|
||||||
if (!user) throw new InternalServerErrorException();
|
if (!user) throw new InternalServerErrorException();
|
||||||
if (!user.email) throw new NotFoundException();
|
if (!user.email) throw new NotFoundException();
|
||||||
const hash = createHash('md5')
|
const hash = createHash("md5")
|
||||||
.update(user.email.trim().toLowerCase())
|
.update(user.email.trim().toLowerCase())
|
||||||
.digest('hex');
|
.digest("hex");
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
|
`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({
|
return this.prisma.likedSongs.findMany({
|
||||||
where: { userId: userId },
|
where: { userId: userId },
|
||||||
|
include: { song: include ? { include } : true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
front/API.ts
16
front/API.ts
@@ -4,7 +4,7 @@ import Chapter from './models/Chapter';
|
|||||||
import Lesson from './models/Lesson';
|
import Lesson from './models/Lesson';
|
||||||
import Genre, { GenreHandler } from './models/Genre';
|
import Genre, { GenreHandler } from './models/Genre';
|
||||||
import LessonHistory from './models/LessonHistory';
|
import LessonHistory from './models/LessonHistory';
|
||||||
import likedSong, { LikedSongHandler } from './models/LikedSong';
|
import likedSong, { LikedSong, LikedSongHandler } from './models/LikedSong';
|
||||||
import Song, { SongHandler, SongInclude } from './models/Song';
|
import Song, { SongHandler, SongInclude } from './models/Song';
|
||||||
import { SongHistoryHandler, SongHistoryItem, SongHistoryItemHandler } from './models/SongHistory';
|
import { SongHistoryHandler, SongHistoryItem, SongHistoryItemHandler } from './models/SongHistory';
|
||||||
import User, { UserHandler } from './models/User';
|
import User, { UserHandler } from './models/User';
|
||||||
@@ -297,13 +297,14 @@ export default class API {
|
|||||||
* Retrieve a song
|
* Retrieve a song
|
||||||
* @param songId the id to find the 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 {
|
return {
|
||||||
key: ['song', songId],
|
key: ['song', songId, include],
|
||||||
exec: async () =>
|
exec: async () =>
|
||||||
API.fetch(
|
API.fetch(
|
||||||
{
|
{
|
||||||
route: `/song/${songId}`,
|
route: `/song/${songId}?include=${include!.join(',')}`,
|
||||||
},
|
},
|
||||||
{ handler: SongHandler }
|
{ handler: SongHandler }
|
||||||
),
|
),
|
||||||
@@ -702,13 +703,14 @@ export default class API {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getLikedSongs(): Query<likedSong[]> {
|
public static getLikedSongs(include?: SongInclude[]): Query<LikedSong[]> {
|
||||||
|
include ??= [];
|
||||||
return {
|
return {
|
||||||
key: ['liked songs'],
|
key: ['liked songs', include],
|
||||||
exec: () =>
|
exec: () =>
|
||||||
API.fetch(
|
API.fetch(
|
||||||
{
|
{
|
||||||
route: '/auth/me/likes',
|
route: `/auth/me/likes?include=${include!.join(',')}`,
|
||||||
},
|
},
|
||||||
{ handler: ListHandler(LikedSongHandler) }
|
{ handler: ListHandler(LikedSongHandler) }
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -70,11 +70,4 @@ const transformQuery = <OldReturnType, NewReturnType>(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useQueries = <ReturnTypes>(
|
export { useQuery, QueryRules, transformQuery };
|
||||||
queries: readonly QueryOrQueryFn<ReturnTypes>[],
|
|
||||||
options?: QueryOptions<ReturnTypes>
|
|
||||||
) => {
|
|
||||||
return RQ.useQueries(queries.map((q) => buildRQuery(q, options)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useQuery, useQueries, QueryRules, transformQuery };
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { HStack, IconButton, Image, Text } from 'native-base';
|
import { HStack, IconButton, Image, Text } from 'native-base';
|
||||||
import RowCustom from './RowCustom';
|
import RowCustom from './RowCustom';
|
||||||
import TextButton from './TextButton';
|
import TextButton from './TextButton';
|
||||||
import { LikedSongWithDetails } from '../models/LikedSong';
|
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import API from '../API';
|
import API from '../API';
|
||||||
import DurationComponent from './DurationComponent';
|
import DurationComponent from './DurationComponent';
|
||||||
|
import Song from '../models/Song';
|
||||||
|
|
||||||
type FavSongRowProps = {
|
type FavSongRowProps = {
|
||||||
FavSong: LikedSongWithDetails; // TODO: remove Song
|
song: Song;
|
||||||
|
addedDate: Date;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
|
||||||
return (
|
return (
|
||||||
<RowCustom width={'100%'}>
|
<RowCustom width={'100%'}>
|
||||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||||
@@ -20,8 +21,8 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
|||||||
flexGrow={0}
|
flexGrow={0}
|
||||||
pl={10}
|
pl={10}
|
||||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
||||||
source={{ uri: FavSong.details.cover }}
|
source={{ uri: song.cover }}
|
||||||
alt={FavSong.details.name}
|
alt={song.name}
|
||||||
borderColor={'white'}
|
borderColor={'white'}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
/>
|
/>
|
||||||
@@ -45,7 +46,7 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
|||||||
bold
|
bold
|
||||||
fontSize="md"
|
fontSize="md"
|
||||||
>
|
>
|
||||||
{FavSong.details.name}
|
{song.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -53,16 +54,16 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
|
|||||||
}}
|
}}
|
||||||
fontSize={'sm'}
|
fontSize={'sm'}
|
||||||
>
|
>
|
||||||
{FavSong.addedDate.toLocaleDateString()}
|
{addedDate.toLocaleDateString()}
|
||||||
</Text>
|
</Text>
|
||||||
<DurationComponent length={FavSong.details.details.length} />
|
<DurationComponent length={song.difficulties.length} />
|
||||||
</HStack>
|
</HStack>
|
||||||
<IconButton
|
<IconButton
|
||||||
colorScheme="primary"
|
colorScheme="primary"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
borderRadius={'full'}
|
borderRadius={'full'}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
API.removeLikedSong(FavSong.songId);
|
API.removeLikedSong(song.id);
|
||||||
}}
|
}}
|
||||||
_icon={{
|
_icon={{
|
||||||
as: MaterialIcons,
|
as: MaterialIcons,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'native-base';
|
} from 'native-base';
|
||||||
import { SafeAreaView } from 'react-native';
|
import { SafeAreaView } from 'react-native';
|
||||||
import { SearchContext } from '../views/SearchView';
|
import { SearchContext } from '../views/SearchView';
|
||||||
import { useQueries, useQuery } from '../Queries';
|
import { useQuery } from '../Queries';
|
||||||
import { translate } from '../i18n/i18n';
|
import { translate } from '../i18n/i18n';
|
||||||
import API from '../API';
|
import API from '../API';
|
||||||
import LoadingComponent, { LoadingView } from './Loading';
|
import LoadingComponent, { LoadingView } from './Loading';
|
||||||
@@ -26,7 +26,6 @@ import { useNavigation } from '../Navigation';
|
|||||||
import Artist from '../models/Artist';
|
import Artist from '../models/Artist';
|
||||||
import SongRow from '../components/SongRow';
|
import SongRow from '../components/SongRow';
|
||||||
import FavSongRow from './FavSongRow';
|
import FavSongRow from './FavSongRow';
|
||||||
import { LikedSongWithDetails } from '../models/LikedSong';
|
|
||||||
|
|
||||||
const swaToSongCardProps = (song: Song) => ({
|
const swaToSongCardProps = (song: Song) => ({
|
||||||
songId: song.id,
|
songId: song.id,
|
||||||
@@ -41,35 +40,7 @@ const HomeSearchComponent = () => {
|
|||||||
API.getSearchHistory(0, 12),
|
API.getSearchHistory(0, 12),
|
||||||
{ enabled: true }
|
{ enabled: true }
|
||||||
);
|
);
|
||||||
const songSuggestions = useQuery(API.getSongSuggestions);
|
const songSuggestions = useQuery(API.getSongSuggestions(['artist']));
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack mt="5" style={{ overflow: 'hidden' }}>
|
<VStack mt="5" style={{ overflow: 'hidden' }}>
|
||||||
@@ -94,11 +65,11 @@ const HomeSearchComponent = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card shadow={3} mt={5} mb={5}>
|
<Card shadow={3} mt={5} mb={5}>
|
||||||
<Heading margin={5}>{translate('songsToGetBetter')}</Heading>
|
<Heading margin={5}>{translate('songsToGetBetter')}</Heading>
|
||||||
{isLoadingSuggestions ? (
|
{!songSuggestions.data ? (
|
||||||
<LoadingComponent />
|
<LoadingComponent />
|
||||||
) : (
|
) : (
|
||||||
<CardGridCustom
|
<CardGridCustom
|
||||||
content={suggestionsData.map(swaToSongCardProps)}
|
content={songSuggestions.data.map(swaToSongCardProps)}
|
||||||
cardComponent={SongCard}
|
cardComponent={SongCard}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -219,19 +190,6 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
|||||||
const FavoritesComponent = () => {
|
const FavoritesComponent = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const favoritesQuery = useQuery(API.getLikedSongs());
|
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) {
|
if (favoritesQuery.isError) {
|
||||||
navigation.navigate('Error');
|
navigation.navigate('Error');
|
||||||
@@ -247,13 +205,14 @@ const FavoritesComponent = () => {
|
|||||||
{translate('songsFilter')}
|
{translate('songsFilter')}
|
||||||
</Text>
|
</Text>
|
||||||
<Box>
|
<Box>
|
||||||
{favSongWithDetails?.map((songData) => (
|
{favoritesQuery.data?.map((songData) => (
|
||||||
<FavSongRow
|
<FavSongRow
|
||||||
key={songData.id}
|
key={songData.id}
|
||||||
FavSong={songData}
|
song={songData.song}
|
||||||
|
addedDate={songData.addedDate}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
API.createSearchHistoryEntry(songData.details!.name, 'song'); //todo
|
API.createSearchHistoryEntry(songData.song.name, 'song'); //todo
|
||||||
navigation.navigate('Play', { songId: songData.details!.id });
|
navigation.navigate('Play', { songId: songData.song!.id });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
import ResponseHandler from './ResponseHandler';
|
import ResponseHandler from './ResponseHandler';
|
||||||
import Song from './Song';
|
import Song, { SongValidator } from './Song';
|
||||||
import Model, { ModelValidator } from './Model';
|
import Model, { ModelValidator } from './Model';
|
||||||
|
|
||||||
export const LikedSongValidator = yup
|
export const LikedSongValidator = yup
|
||||||
.object({
|
.object({
|
||||||
songId: yup.number().required(),
|
songId: yup.number().required(),
|
||||||
|
song: yup.lazy(() => SongValidator.default(undefined)),
|
||||||
addedDate: yup.date().required(),
|
addedDate: yup.date().required(),
|
||||||
})
|
})
|
||||||
.concat(ModelValidator);
|
.concat(ModelValidator);
|
||||||
|
|
||||||
export const LikedSongHandler: ResponseHandler<
|
export type LikedSong = yup.InferType<typeof LikedSongValidator>;
|
||||||
yup.InferType<typeof LikedSongValidator>,
|
|
||||||
LikedSong
|
|
||||||
> = {
|
|
||||||
validator: LikedSongValidator,
|
|
||||||
transformer: (likedSong) => ({
|
|
||||||
id: likedSong.id,
|
|
||||||
songId: likedSong.songId,
|
|
||||||
addedDate: likedSong.addedDate,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
interface LikedSong extends Model {
|
|
||||||
songId: number;
|
|
||||||
addedDate: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LikedSongWithDetails extends LikedSong {
|
export const LikedSongHandler: ResponseHandler<LikedSong> = {
|
||||||
details: Song;
|
validator: LikedSongValidator,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LikedSong;
|
export default LikedSong;
|
||||||
|
|||||||
@@ -22,26 +22,25 @@ import { LoadingView } from '../components/Loading';
|
|||||||
|
|
||||||
export const FavoritesMusic = () => {
|
export const FavoritesMusic = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const playHistoryQuery = useQuery(API.getUserPlayHistory(['artist']));
|
const likedSongs = useQuery(API.getLikedSongs(['artist']));
|
||||||
|
|
||||||
const musics =
|
const musics =
|
||||||
playHistoryQuery.data
|
likedSongs.data
|
||||||
?.map((x) => x.song)
|
?.map((x) => ({
|
||||||
.map((song: Song) => ({
|
artist: x.song.artist!.name,
|
||||||
artist: song.artist!.name,
|
song: x.song.name,
|
||||||
song: song.name,
|
image: x.song.cover,
|
||||||
image: song.cover,
|
|
||||||
level: 42,
|
level: 42,
|
||||||
lastScore: 42,
|
lastScore: 42,
|
||||||
bestScore: 42,
|
bestScore: 42,
|
||||||
liked: false,
|
liked: true,
|
||||||
onLike: () => {
|
onLike: () => {
|
||||||
console.log('onLike');
|
console.log('onLike');
|
||||||
},
|
},
|
||||||
onPlay: () => navigation.navigate('Play', { songId: song.id }),
|
onPlay: () => navigation.navigate('Play', { songId: x.song.id }),
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
if (playHistoryQuery.isLoading) {
|
if (likedSongs.isLoading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import TextButton from '../components/TextButton';
|
|||||||
import API from '../API';
|
import API from '../API';
|
||||||
import CardGridCustom from '../components/CardGridCustom';
|
import CardGridCustom from '../components/CardGridCustom';
|
||||||
import SongCard from '../components/SongCard';
|
import SongCard from '../components/SongCard';
|
||||||
import { useQueries, useQuery } from '../Queries';
|
import { useQuery } from '../Queries';
|
||||||
import { LoadingView } from '../components/Loading';
|
import { LoadingView } from '../components/Loading';
|
||||||
import ScoreGraph from '../components/ScoreGraph';
|
import ScoreGraph from '../components/ScoreGraph';
|
||||||
|
|
||||||
@@ -29,23 +29,10 @@ type ScoreViewProps = {
|
|||||||
const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
||||||
const { songId, overallScore, precision, score } = props;
|
const { songId, overallScore, precision, score } = props;
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const songQuery = useQuery(API.getSong(songId));
|
const songQuery = useQuery(API.getSong(songId, ['artist']));
|
||||||
const artistQuery = useQuery(() => API.getArtist(songQuery.data!.artistId!), {
|
const recommendations = useQuery(API.getSongSuggestions(['artist']));
|
||||||
enabled: songQuery.data !== undefined,
|
|
||||||
});
|
|
||||||
const recommendations = useQuery(API.getSongSuggestions);
|
|
||||||
const artistRecommendations = useQueries(
|
|
||||||
recommendations.data
|
|
||||||
?.filter(({ artistId }) => artistId !== null)
|
|
||||||
.map((song) => API.getArtist(song.artistId)) ?? []
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (!recommendations.data || !songQuery.data) {
|
||||||
!recommendations.data ||
|
|
||||||
artistRecommendations.find(({ data }) => !data) ||
|
|
||||||
!songQuery.data ||
|
|
||||||
(songQuery.data.artistId && !artistQuery.data)
|
|
||||||
) {
|
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
if (songQuery.isError) {
|
if (songQuery.isError) {
|
||||||
@@ -59,7 +46,7 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
|||||||
<Text bold fontSize="lg">
|
<Text bold fontSize="lg">
|
||||||
{songQuery.data.name}
|
{songQuery.data.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text bold>{artistQuery.data?.name}</Text>
|
<Text bold>{songQuery.data.artist!.name}</Text>
|
||||||
<Row style={{ justifyContent: 'center', display: 'flex' }}>
|
<Row style={{ justifyContent: 'center', display: 'flex' }}>
|
||||||
<Card shadow={3} style={{ flex: 1 }}>
|
<Card shadow={3} style={{ flex: 1 }}>
|
||||||
<Image
|
<Image
|
||||||
@@ -144,9 +131,7 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
|||||||
content={recommendations.data.map((i) => ({
|
content={recommendations.data.map((i) => ({
|
||||||
cover: i.cover,
|
cover: i.cover,
|
||||||
name: i.name,
|
name: i.name,
|
||||||
artistName:
|
artistName: i.artist!.name,
|
||||||
artistRecommendations.find(({ data }) => data?.id == i.artistId)?.data
|
|
||||||
?.name ?? '',
|
|
||||||
songId: i.id,
|
songId: i.id,
|
||||||
}))}
|
}))}
|
||||||
cardComponent={SongCard}
|
cardComponent={SongCard}
|
||||||
|
|||||||
Reference in New Issue
Block a user